import Foundation import Testing @testable import gitbay private func makeClient() throws -> (GitbayClient, StubProtocol.Box) { let box = StubProtocol.box() let client = GitbayClient( instance: try GitbayInstance(url: "https://gitbay.org"), token: "test-token", session: box.session() ) return (client, box) } private func body(_ seen: StubProtocol.Seen) throws -> [String: Any] { try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) } private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# private let secretsJSON = #"{"protocol_version":1,"data":["DEPLOY_TOKEN","NPM_TOKEN"],"exit_code":0}"# private let deployKeysJSON = """ {"protocol_version":1,"data":[\ {"fingerprint":"SHA256:abc","algo":"ssh-ed25519","mode":"rw","label":"ci@box","last_used_at":"2026-10-10T08:00:00.000Z"},\ {"fingerprint":"SHA256:def","algo":"ssh-ed25519","mode":"ro","label":"","expires_at":"2026-11-10T08:00:00Z"}\ ],"exit_code":0} """ @MainActor struct BuildSecretsViewModelTests { @Test func listsSecretNames() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: secretsJSON)) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") await model.load() #expect(model.state.value == ["DEPLOY_TOKEN", "NPM_TOKEN"]) #expect(stub.seen.first?.url.query() == "argv=repo&argv=secret&argv=list&argv=krz/gitbay") } @Test func noSecretsIsAnEmptyState() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":null,"exit_code":0}"#)) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") await model.load() guard case .empty = model.state else { Issue.record("expected .empty, got \(model.state)") return } } @Test func setSendsTheValueOnStdinNeverInArgv() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"secret":"DEPLOY_TOKEN"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list")) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.set(name: " DEPLOY_TOKEN ", value: "s3cret")) let sent = try body(try #require(stub.seen.first)) #expect(sent["argv"] as? [String] == ["repo", "secret", "set", "krz/gitbay", "DEPLOY_TOKEN"]) #expect(sent["stdin"] as? String == "s3cret") #expect(model.state.value == ["DEPLOY_TOKEN", "NPM_TOKEN"]) } @Test func aRefusedNameSaysWhy() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"secret names are env-var shaped: uppercase letters, digits, _","exit_code":2}"#)) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.set(name: "lower", value: "x") == false) #expect(model.actionError == "secret names are env-var shaped: uppercase letters, digits, _") } @Test func aFailedWriteStillRefreshesTheList() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no secret NPM_TOKEN on krz/gitbay","exit_code":3}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":["DEPLOY_TOKEN"],"exit_code":0}"#, match: "argv=list")) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") await model.remove("NPM_TOKEN") #expect(model.actionError == "no secret NPM_TOKEN on krz/gitbay") #expect(model.state.value == ["DEPLOY_TOKEN"]) } @Test func aMultiLineValueKeepsItsNewlines() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list")) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") let key = "-----BEGIN KEY-----\nabc\n-----END KEY-----\n" #expect(await model.set(name: "BOT_SSH_KEY", value: key)) #expect(try body(try #require(stub.seen.first))["stdin"] as? String == key) } @Test func removeNamesTheSecret() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list")) let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay") await model.remove("NPM_TOKEN") #expect(try body(try #require(stub.seen.first))["argv"] as? [String] == ["repo", "secret", "remove", "krz/gitbay", "NPM_TOKEN"]) } } @MainActor struct DeployKeysViewModelTests { @Test func listsKeysWithModeAndDates() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: deployKeysJSON)) let model = DeployKeysViewModel(client: client, repoPath: "krz/gitbay") await model.load() let keys = try #require(model.state.value) #expect(keys.map(\.fingerprint) == ["SHA256:abc", "SHA256:def"]) #expect(keys[0].canPush && !keys[1].canPush) #expect(keys[0].lastUsedAt != nil && keys[0].expiresAt == nil) #expect(keys[1].lastUsedAt == nil && keys[1].expiresAt != nil) #expect(stub.seen.first?.url.query() == "argv=repo&argv=deploy-key&argv=list&argv=krz/gitbay") } @Test func addSendsTheKeyOnStdinWithModeAndLifetime() async throws { let (client, stub) = try makeClient() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"fingerprint":"SHA256:x","mode":"ro"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: deployKeysJSON, match: "argv=list")) } let model = DeployKeysViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.add(publicKey: "ssh-ed25519 AAAA ci\n", canPush: false, ttl: nil)) #expect(await model.add(publicKey: "ssh-ed25519 AAAA ci", canPush: true, ttl: "30d")) let writes = try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map(body) #expect(writes.map { $0["argv"] as? [String] } == [ ["repo", "deploy-key", "add", "krz/gitbay"], ["repo", "deploy-key", "add", "krz/gitbay", "--rw", "--ttl", "30d"], ]) #expect(writes.allSatisfy { $0["stdin"] as? String == "ssh-ed25519 AAAA ci" }) } @Test func removeNamesTheFingerprint() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: deployKeysJSON, match: "argv=list")) stub.enqueue(.init(status: 200, json: deployKeysJSON, match: "argv=list")) let model = DeployKeysViewModel(client: client, repoPath: "krz/gitbay") await model.load() await model.remove(try #require(model.state.value?.first)) let writes = try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map(body) #expect(writes.first?["argv"] as? [String] == ["repo", "deploy-key", "remove", "krz/gitbay", "SHA256:abc"]) } }