krz/keycask

Password manager: Swift core library, CLI for macOS/Linux/Windows, iOS/macOS app. cli password-manager swift

Tests/KeycaskCLITests/CLI.swift

94 lines · 3375 bytes

 1import Foundation
 2import Testing
 3
 4enum Binary {
 5    static let url: URL = {
 6        #if os(macOS)
 7            if let index = CommandLine.arguments.firstIndex(of: "--test-bundle-path"),
 8                CommandLine.arguments.count > index + 1
 9            {
10                return URL(fileURLWithPath: CommandLine.arguments[index + 1])
11                    .deletingLastPathComponent()  // MacOS
12                    .deletingLastPathComponent()  // Contents
13                    .deletingLastPathComponent()  // *.xctest
14                    .deletingLastPathComponent()  // Products/Debug
15                    .appendingPathComponent("keycask")
16            }
17            for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
18                return bundle.bundleURL.deletingLastPathComponent().appendingPathComponent(
19                    "keycask")
20            }
21            fatalError("test bundle not found")
22        #elseif os(Windows)
23            return Bundle.main.bundleURL.appendingPathComponent("keycask.exe")
24        #else
25            return Bundle.main.bundleURL.appendingPathComponent("keycask")
26        #endif
27    }()
28}
29
30struct CLI {
31    struct Result {
32        let status: Int32
33        let stdout: String
34        let stderr: String
35        var lines: [String] { stdout.split(separator: "\n").map(String.init) }
36    }
37
38    static let passphrase = "correct horse battery"
39
40    let dir: URL
41    let vault: URL
42
43    init() throws {
44        dir = FileManager.default.temporaryDirectory
45            .appendingPathComponent("keycask-tests-\(UUID().uuidString)")
46        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
47        vault = dir.appendingPathComponent("vault.kc")
48    }
49
50    @discardableResult
51    func run(
52        _ args: [String],
53        stdin: String? = nil,
54        passphrase: String? = CLI.passphrase,
55        extraEnvironment: [String: String] = [:]
56    ) throws -> Result {
57        let process = Process()
58        process.executableURL = Binary.url
59        process.arguments = args
60        var env = ProcessInfo.processInfo.environment
61        env["KEYCASK_VAULT"] = vault.path
62        env.removeValue(forKey: "KEYCASK_PASSPHRASE")
63        if let passphrase { env["KEYCASK_PASSPHRASE"] = passphrase }
64        for (k, v) in extraEnvironment { env[k] = v }
65        process.environment = env
66
67        let out = Pipe()
68        let err = Pipe()
69        let input = Pipe()
70        process.standardOutput = out
71        process.standardError = err
72        process.standardInput = input
73        try process.run()
74        if let stdin {
75            input.fileHandleForWriting.write(Data(stdin.utf8))
76        }
77        try input.fileHandleForWriting.close()
78        let outData = out.fileHandleForReading.readDataToEndOfFile()
79        let errData = err.fileHandleForReading.readDataToEndOfFile()
80        process.waitUntilExit()
81        return Result(
82            status: process.terminationStatus,
83            stdout: String(decoding: outData, as: UTF8.self),
84            stderr: String(decoding: errData, as: UTF8.self))
85    }
86
87    /// Runs `init` and returns the harness, for tests that need a vault.
88    static func initialized() throws -> CLI {
89        let cli = try CLI()
90        let r = try cli.run(["init"])
91        precondition(r.status == 0, "init failed: \(r.stderr)")
92        return cli
93    }
94}