@_spi(Internal) import CliClient import ConfigurationClient import Dependencies import FileClient import Foundation import ShellClient import Testing import TestSupport @Suite("CliClientTests") struct CliClientTests: TestCase { static let loggingOptions: CliClient.LoggingOptions = { let levelString = ProcessInfo.processInfo.environment["LOGGING_LEVEL"] ?? "debug" let logLevel = Logger.Level(rawValue: levelString) ?? .debug return .init(commandName: "CliClientTests", logLevel: logLevel) }() @Test func capturingClient() async throws { let captured = CliClient.CapturingClient() let client = CliClient.capturing(captured) try await client.runCommand(quiet: false, shell: .zsh(), "foo", "bar") let quiet = await captured.quiet! #expect(quiet == false) let shell = await captured.shell #expect(shell == .zsh()) let arguments = await captured.arguments! #expect(arguments == ["foo", "bar"]) } @Test(arguments: ["encrypt", "decrypt"]) func runVault(argument: String) async throws { let captured = CliClient.CapturingClient() try await withMockConfiguration(captured, key: "runVault") { $0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "vault.yml") } } operation: { @Dependency(\.cliClient) var cliClient let configuration = Configuration.mock try await cliClient.runVaultCommand( .init(arguments: [argument], quiet: false, shell: nil), logging: Self.loggingOptions ) let shell = await captured.shell #expect(shell == .zsh(useDashC: true)) let vaultPath = URL(filePath: #file) .deletingLastPathComponent() .deletingLastPathComponent() .appending(path: "vault.yml") var encryptArgs: [String] = [] if argument == "encrypt", let id = configuration.vault.encryptId { encryptArgs = ["--encrypt-vault-id", id] } let expectedArguments = [ "ansible-vault", argument ] + configuration.vault.args! + encryptArgs + [vaultPath.cleanFilePath] let arguments = await captured.arguments #expect(arguments == expectedArguments) } } @Test(arguments: [ Configuration( args: ["--tags", "debug"], useVaultArgs: true, playbook: .init(directory: "playbook", inventory: nil), vault: .mock ) ]) func runPlaybook(configuration: Configuration) async throws { let captured = CliClient.CapturingClient() try await withMockConfiguration(captured, configuration: configuration, key: "runPlaybook") { @Dependency(\.cliClient) var cliClient try await cliClient.runPlaybookCommand( .init( arguments: [], quiet: false, shell: nil ), logging: Self.loggingOptions ) let expectedArguments = [ "ansible-playbook", "playbook/main.yml", "--inventory", "playbook/inventory.ini", "--tags", "debug", "--vault-id=myId@$SCRIPTS/vault-gopass-client" ] let arguments = await captured.arguments #expect(arguments == expectedArguments) } } @Test func ensuredPlaybookDirectory() throws { let configuration = Configuration.mock let playbookDir = try configuration.ensuredPlaybookDirectory("playbook") #expect(playbookDir == "playbook") do { _ = try configuration.ensuredPlaybookDirectory(nil) #expect(Bool(false)) } catch { #expect(Bool(true)) } } @Test func shellOrDefault() { var shell: String? = "/bin/bash" #expect(shell.orDefault == .custom(path: "/bin/bash", useDashC: true)) shell = nil #expect(shell.orDefault == .zsh(useDashC: true)) } @Test func testEnsuredInventoryPath() { let configuration = Configuration(playbook: .init(inventory: "inventory.ini")) let playbookDir = "playbook" let inventoryPath = "inventory.ini" var output = ensuredInventoryPath( inventoryPath, configuration: configuration, playbookDirectory: playbookDir ) #expect(output == "inventory.ini") output = ensuredInventoryPath( nil, configuration: configuration, playbookDirectory: playbookDir ) #expect(output == "inventory.ini") } @Test func vaultFilePath() async throws { var fileClient = FileClient.testValue fileClient.findVaultFileInCurrentDirectory = { URL(string: "vault.yml") } var output = try await fileClient.ensuredVaultFilePath("vault.yml") #expect(output == "vault.yml") output = try await fileClient.ensuredVaultFilePath(nil) fileClient.findVaultFileInCurrentDirectory = { nil } do { _ = try await fileClient.ensuredVaultFilePath(nil) #expect(Bool(false)) } catch { #expect(Bool(true)) } } @Test( arguments: [ GenerateJsonTestOption( options: .init( templateDirectory: nil, templateRepo: nil, version: nil, useLocalTemplateDirectory: true ), configuration: nil, expectation: .failing ), GenerateJsonTestOption( options: .init( templateDirectory: nil, templateRepo: nil, version: nil, useLocalTemplateDirectory: false ), configuration: nil, expectation: .failing ), GenerateJsonTestOption( options: .init( templateDirectory: "template", templateRepo: nil, version: nil, useLocalTemplateDirectory: true ), configuration: nil, expectation: .success(""" { "template" : { "path" : "template" } } """) ), GenerateJsonTestOption( options: .init( templateDirectory: nil, templateRepo: nil, version: nil, useLocalTemplateDirectory: false ), configuration: .init(template: .init(directory: "template")), expectation: .success(""" { "template" : { "path" : "template" } } """) ), GenerateJsonTestOption( options: .init( templateDirectory: nil, templateRepo: "https://git.example.com/template.git", version: nil, useLocalTemplateDirectory: false ), configuration: nil, expectation: .success(""" { "template" : { "repo" : { "url" : "https://git.example.com/template.git", "version" : "main" } } } """) ), GenerateJsonTestOption( options: .init( templateDirectory: nil, templateRepo: nil, version: nil, useLocalTemplateDirectory: false ), configuration: .init(template: .init(url: "https://git.example.com/template.git", version: "v0.1.0")), expectation: .success(""" { "template" : { "repo" : { "url" : "https://git.example.com/template.git", "version" : "v0.1.0" } } } """) ) ] ) func generateJson(input: GenerateJsonTestOption) async { await withTestLogger(key: "generateJson") { $0.configurationClient = .mock(input.configuration ?? .init()) $0.cliClient = .liveValue } operation: { @Dependency(\.cliClient) var cliClient let json = try? await cliClient.generateJSON( input.options, logging: Self.loggingOptions, encoder: jsonEncoder ) switch input.expectation { case let .success(expected): #expect(json == expected) case .failing: #expect(json == nil) } } } func withMockConfiguration( _ capturing: CliClient.CapturingClient, configuration: Configuration = .mock, key: String, logLevel: Logger.Level = .trace, depednencies setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in }, operation: @Sendable @escaping () async throws -> Void ) async rethrows { try await withTestLogger(key: key, logLevel: logLevel) { $0.configurationClient = .mock(configuration) $0.cliClient = .capturing(capturing) setupDependencies(&$0) } operation: { try await operation() } } } struct GenerateJsonTestOption: Sendable { let options: CliClient.GenerateJsonOptions let configuration: Configuration? let expectation: GenerateJsonExpectation } enum GenerateJsonExpectation: Sendable { case failing case success(String) } extension ConfigurationClient { static func mock(_ configuration: Configuration) -> Self { var mock = Self.testValue mock.find = { throw TestError() } mock.load = { _ in configuration } return mock } } struct TestError: Error {} let jsonEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys] return encoder }()