import CodersClient @_spi(Internal) import CommandClient @_spi(Internal) import ConfigurationClient import Dependencies import FileClient import Foundation @_spi(Internal) import PlaybookClient import ShellClient import Testing import TestSupport @Suite("PlaybookClientTests") struct PlaybookClientTests: TestCase { static var sharedRunOptions: PlaybookClient.RunPlaybook.SharedRunOptions { .init(loggingOptions: loggingOptions) } static let defaultPlaybookPath = "~/.local/share/hpa/playbook/main.yml" static let defaultInventoryPath = "~/.local/share/hpa/playbook/inventory.ini" static let mockVaultArg = Configuration.mock.vault.args![0] @Test(.tags(.repository)) func repositoryInstallation() async throws { try await withTestLogger(key: "repositoryInstallation") { $0.fileClient = .liveValue $0.asyncShellClient = .liveValue $0.commandClient = .liveValue } operation: { try await withTemporaryDirectory { tempDirectory in @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger let pathUrl = tempDirectory.appending(path: "playbook") let playbookClient = PlaybookClient.liveValue let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath)) try? FileManager.default.removeItem(at: pathUrl) try await playbookClient.repository.install(configuration) logger.debug("Done cloning playbook") let exists = try await fileClient.isDirectory(pathUrl) #expect(exists) } } } @Test( .tags(.repository), arguments: [ (Configuration(), PlaybookClient.Constants.defaultInstallationPath), (Configuration(playbook: .init(directory: "playbook")), "playbook") ] ) func repositoryDirectory(configuration: Configuration, expected: String) async throws { let client = PlaybookClient.liveValue let result = try await client.repository.directory(configuration) #expect(result == expected) } @Test(.tags(.run)) func runBuildProject() async throws { let captured = CommandClient.CapturingClient() try await withMockConfiguration(captured, key: "runBuildProject") { @Dependency(\.playbookClient) var playbookClient try await playbookClient.run.buildProject(.init(projectDirectory: "/foo", shared: Self.sharedRunOptions)) let arguments = await captured.options!.arguments print(arguments) #expect(arguments == [ "ansible-playbook", Self.defaultPlaybookPath, "--inventory", Self.defaultInventoryPath, Self.mockVaultArg, "--tags", "build-project", "--extra-vars", "project_dir=/foo" ]) } } @Test( .tags(.run), arguments: [ (true, "\'{\"template\":{\"path\":\"\(Configuration.mock.template.directory!)\"}}\'"), (false, "\'{\"template\":{\"repo\":{\"url\":\"\(Configuration.mock.template.url!)\",\"version\":\"\(Configuration.mock.template.version!)\"}}}\'") ] ) func runCreateProject(useLocalTemplateDirectory: Bool, json: String) async throws { let captured = CommandClient.CapturingClient() try await withMockConfiguration(captured, key: "runBuildProject") { @Dependency(\.logger) var logger @Dependency(\.playbookClient) var playbookClient try await playbookClient.run.createProject( .init( projectDirectory: "/project", shared: Self.sharedRunOptions, useLocalTemplateDirectory: useLocalTemplateDirectory ) ) let arguments = await captured.options!.arguments logger.debug("\(arguments)") #expect(arguments == [ "ansible-playbook", Self.defaultPlaybookPath, "--inventory", Self.defaultInventoryPath, Self.mockVaultArg, "--tags", "setup-project", "--extra-vars", "project_dir=/project", "--extra-vars", json ]) } } @Test(arguments: CreateJsonTestOption.testCases) func createJson(input: CreateJsonTestOption) { withTestLogger(key: "generateJson") { $0.coders.jsonEncoder = { jsonEncoder } $0.configurationClient = .mock(input.configuration) } operation: { @Dependency(\.coders) var coders let jsonData = try? input.options.createJSONData( configuration: input.configuration, encoder: coders.jsonEncoder() ) switch input.expectation { case let .success(expected): let json = String(data: jsonData!, encoding: .utf8)! if json != expected { print("json:", json) print("expected:", expected) } #expect(json == expected) case .failure: #expect(jsonData == nil) } } } @Test func generateTemplate() async throws { try await withCapturingCommandClient("generateTemplate") { $0.fileClient.isDirectory = { _ in true } $0.configurationClient = .mock() $0.playbookClient = .liveValue } run: { @Dependency(\.playbookClient) var playbookClient let output = try await playbookClient.run.generateTemplate(.init( shared: Self.sharedRunOptions, templateDirectory: "/template" )) #expect(output == "/template") } assert: { output in let expected = [ "ansible-playbook", Self.defaultPlaybookPath, "--inventory", Self.defaultInventoryPath, Self.mockVaultArg, "--tags", "repo-template", "--extra-vars", "output_dir=/template" ] #expect(output.arguments == expected) } } func withMockConfiguration( _ capturing: CommandClient.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 withDependencies { $0.fileClient.isDirectory = { _ in true } $0.configurationClient = .mock(configuration) $0.commandClient = .capturing(capturing) $0.playbookClient = .liveValue setupDependencies(&$0) } operation: { try await operation() } } } struct CreateJsonTestOption: Sendable { let options: PlaybookClient.RunPlaybook.CreateOptions let configuration: Configuration let expectation: Result static let testCases: [Self] = [ CreateJsonTestOption( options: .init( projectDirectory: "/project", shared: PlaybookClientTests.sharedRunOptions, template: .init(url: nil, version: nil, directory: nil), useLocalTemplateDirectory: true ), configuration: .init(), expectation: .failing ), CreateJsonTestOption( options: .init( projectDirectory: "/project", shared: PlaybookClientTests.sharedRunOptions, template: .init(url: nil, version: nil, directory: nil), useLocalTemplateDirectory: false ), configuration: .init(), expectation: .failing ), CreateJsonTestOption( options: .init( projectDirectory: "/project", shared: PlaybookClientTests.sharedRunOptions, template: .init(url: nil, version: nil, directory: "/template"), useLocalTemplateDirectory: true ), configuration: .init(template: .init(directory: "/template")), expectation: .success(""" { "template" : { "path" : "/template" } } """) ), CreateJsonTestOption( options: .init( projectDirectory: "/project", shared: PlaybookClientTests.sharedRunOptions, template: .init(url: nil, version: nil, directory: "/template"), useLocalTemplateDirectory: true ), configuration: .init(template: .init(directory: "/template")), expectation: .success(""" { "template" : { "path" : "/template" } } """) ), CreateJsonTestOption( options: .init( projectDirectory: "/project", shared: PlaybookClientTests.sharedRunOptions, template: .init(url: "https://git.example.com/template.git", version: "main", directory: nil), useLocalTemplateDirectory: false ), configuration: .init(), expectation: .success(""" { "template" : { "repo" : { "url" : "https://git.example.com/template.git", "version" : "main" } } } """) ), CreateJsonTestOption( options: .init( projectDirectory: "/project", shared: PlaybookClientTests.sharedRunOptions, template: .init(url: "https://git.example.com/template.git", version: "v0.1.0", directory: 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" } } } """) ) ] } extension Result where Failure == TestError { static var failing: Self { .failure(TestError()) } } struct TestError: Error {} extension Tag { @Tag static var repository: Self @Tag static var run: Self } let jsonEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys] return encoder }()