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 writes(_ stub: StubProtocol.Box) throws -> [[String: Any]] { try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map { try #require(try JSONSerialization.jsonObject(with: $0.body) as? [String: Any]) } } private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# private let mirrorsJSON = """ {"protocol_version":1,"data":[\ {"id":1,"direction":"push","url":"https://github.com/krz/gitbay.git","username":"bot","pending":true},\ {"id":2,"direction":"pull","url":"https://example.test/up.git","pending":false,\ "last_sync":"2026-10-15T10:00:00.123Z","last_error":"authentication failed"}\ ],"exit_code":0} """ private let domainsJSON = """ {"protocol_version":1,"data":[\ {"domain":"docs.example.test","state":"pending"},\ {"domain":"www.example.test","state":"verified","verified_at":"2026-10-01T09:00:00Z"},\ {"domain":"old.example.test","state":"expired"}\ ],"exit_code":0} """ @MainActor struct MirrorsViewModelTests { @Test func listsMirrorsWithTheirSyncState() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: mirrorsJSON)) let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay") await model.load() let mirrors = try #require(model.state.value) #expect(mirrors.map(\.id) == [1, 2]) #expect(mirrors[0].pending && mirrors[0].lastSync == nil && mirrors[0].username == "bot") #expect(mirrors[1].isPull && mirrors[1].lastSync != nil && mirrors[1].lastError == "authentication failed") #expect(stub.seen.first?.url.query() == "argv=repo&argv=mirror&argv=list&argv=krz/gitbay") } @Test func addSendsTheTokenOnStdinOnlyWhenGiven() async throws { let (client, stub) = try makeClient() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"id":3},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: mirrorsJSON, match: "argv=list")) } let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.add(url: " https://example.test/a.git ", pull: false, username: "", token: "")) #expect(await model.add(url: "https://example.test/b.git", pull: true, username: "bot", token: "t0k")) let sent = try writes(stub) #expect(sent.map { $0["argv"] as? [String] } == [ ["repo", "mirror", "add", "krz/gitbay", "https://example.test/a.git", "--direction", "push"], ["repo", "mirror", "add", "krz/gitbay", "https://example.test/b.git", "--direction", "pull", "--username", "bot", "--token-stdin"], ]) #expect(sent[0]["stdin"] == nil) #expect(sent[1]["stdin"] as? String == "t0k\n") } @Test func aRefusedURLSaysWhy() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"that mirror already exists","exit_code":2}"#)) let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.add(url: "https://example.test/a.git", pull: false, username: "", token: "") == false) #expect(model.actionError == "that mirror already exists") } @Test func syncAndRemove() async throws { let (client, stub) = try makeClient() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: mirrorsJSON, match: "argv=list")) } let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.sync()) await model.remove(id: 2) #expect(try writes(stub).map { $0["argv"] as? [String] } == [ ["repo", "mirror", "sync", "krz/gitbay"], ["repo", "mirror", "remove", "krz/gitbay", "2"], ]) } } @MainActor struct PageDomainsViewModelTests { @Test func listsClaimsWithTheirState() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: domainsJSON)) let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay") await model.load() let domains = try #require(model.state.value) #expect(domains.map(\.domain) == ["docs.example.test", "www.example.test", "old.example.test"]) #expect(domains[0].isPending && domains[0].verifiedAt == nil) #expect(domains[1].isVerified && domains[1].verifiedAt != nil) #expect(!domains[2].isPending && !domains[2].isVerified) } @Test func addReturnsTheDNSChallenge() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"challenge_name":"_gitbay-challenge.docs.example.test",\ "challenge_value":"gitbay-domain-verify=abc","domain":"docs.example.test","state":"pending"},"exit_code":0} """, match: "cmd")) stub.enqueue(.init(status: 200, json: domainsJSON, match: "argv=list")) let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay") let challenge = try #require(await model.add(" Docs.Example.Test ")) #expect(challenge.name == "_gitbay-challenge.docs.example.test") #expect(challenge.value == "gitbay-domain-verify=abc") #expect(try writes(stub).first?["argv"] as? [String] == ["repo", "domain", "add", "krz/gitbay", "Docs.Example.Test"]) } @Test func verifyReportsAMissingRecordVerbatim() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"_gitbay-challenge.docs.example.test does not carry the expected record \"x\"","exit_code":4}"#)) let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.verify("docs.example.test") == false) #expect(model.actionError == #"_gitbay-challenge.docs.example.test does not carry the expected record "x""#) } @Test func aRefusedVerifyRefreshesTheList() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"the claim on old.example.test expired; run repo domain add again","exit_code":2}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[],"exit_code":0}"#, match: "argv=list")) let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.verify("old.example.test") == false) #expect(model.actionError == "the claim on old.example.test expired; run repo domain add again") guard case .empty = model.state else { Issue.record("expected the removed claim gone, got \(model.state)") return } } @Test func verifyAndRemoveNameTheDomain() async throws { let (client, stub) = try makeClient() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"domain":"d","state":"verified"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: domainsJSON, match: "argv=list")) } let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay") #expect(await model.verify("docs.example.test")) await model.remove("www.example.test") #expect(try writes(stub).map { $0["argv"] as? [String] } == [ ["repo", "domain", "verify", "krz/gitbay", "docs.example.test"], ["repo", "domain", "remove", "krz/gitbay", "www.example.test"], ]) } }