gitbayTests/RepoMirrorDomainTests.swift
173 lines · 8111 bytes
13 symbols in this file
makeClientwritesMirrorsViewModelTestslistsMirrorsWithTheirSyncStateaddSendsTheTokenOnStdinOnlyWhenGivenaRefusedURLSaysWhysyncAndRemovePageDomainsViewModelTestslistsClaimsWithTheirStateaddReturnsTheDNSChallengeverifyReportsAMissingRecordVerbatimaRefusedVerifyRefreshesTheListverifyAndRemoveNameTheDomain
1import Foundation
2import Testing
3@testable import gitbay
4
5private func makeClient() throws -> (GitbayClient, StubProtocol.Box) {
6 let box = StubProtocol.box()
7 let client = GitbayClient(
8 instance: try GitbayInstance(url: "https://gitbay.org"),
9 token: "test-token",
10 session: box.session()
11 )
12 return (client, box)
13}
14
15private func writes(_ stub: StubProtocol.Box) throws -> [[String: Any]] {
16 try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map {
17 try #require(try JSONSerialization.jsonObject(with: $0.body) as? [String: Any])
18 }
19}
20
21private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
22private let mirrorsJSON = """
23 {"protocol_version":1,"data":[\
24 {"id":1,"direction":"push","url":"https://github.com/krz/gitbay.git","username":"bot","pending":true},\
25 {"id":2,"direction":"pull","url":"https://example.test/up.git","pending":false,\
26 "last_sync":"2026-10-15T10:00:00.123Z","last_error":"authentication failed"}\
27 ],"exit_code":0}
28 """
29private let domainsJSON = """
30 {"protocol_version":1,"data":[\
31 {"domain":"docs.example.test","state":"pending"},\
32 {"domain":"www.example.test","state":"verified","verified_at":"2026-10-01T09:00:00Z"},\
33 {"domain":"old.example.test","state":"expired"}\
34 ],"exit_code":0}
35 """
36
37@MainActor
38struct MirrorsViewModelTests {
39
40 @Test func listsMirrorsWithTheirSyncState() async throws {
41 let (client, stub) = try makeClient()
42 stub.enqueue(.init(status: 200, json: mirrorsJSON))
43 let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay")
44 await model.load()
45
46 let mirrors = try #require(model.state.value)
47 #expect(mirrors.map(\.id) == [1, 2])
48 #expect(mirrors[0].pending && mirrors[0].lastSync == nil && mirrors[0].username == "bot")
49 #expect(mirrors[1].isPull && mirrors[1].lastSync != nil && mirrors[1].lastError == "authentication failed")
50 #expect(stub.seen.first?.url.query() == "argv=repo&argv=mirror&argv=list&argv=krz/gitbay")
51 }
52
53 @Test func addSendsTheTokenOnStdinOnlyWhenGiven() async throws {
54 let (client, stub) = try makeClient()
55 for _ in 0..<2 {
56 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"id":3},"exit_code":0}"#, match: "cmd"))
57 stub.enqueue(.init(status: 200, json: mirrorsJSON, match: "argv=list"))
58 }
59 let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay")
60
61 #expect(await model.add(url: " https://example.test/a.git ", pull: false, username: "", token: ""))
62 #expect(await model.add(url: "https://example.test/b.git", pull: true, username: "bot", token: "t0k"))
63
64 let sent = try writes(stub)
65 #expect(sent.map { $0["argv"] as? [String] } == [
66 ["repo", "mirror", "add", "krz/gitbay", "https://example.test/a.git", "--direction", "push"],
67 ["repo", "mirror", "add", "krz/gitbay", "https://example.test/b.git", "--direction", "pull",
68 "--username", "bot", "--token-stdin"],
69 ])
70 #expect(sent[0]["stdin"] == nil)
71 #expect(sent[1]["stdin"] as? String == "t0k\n")
72 }
73
74 @Test func aRefusedURLSaysWhy() async throws {
75 let (client, stub) = try makeClient()
76 stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"that mirror already exists","exit_code":2}"#))
77 let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay")
78
79 #expect(await model.add(url: "https://example.test/a.git", pull: false, username: "", token: "") == false)
80 #expect(model.actionError == "that mirror already exists")
81 }
82
83 @Test func syncAndRemove() async throws {
84 let (client, stub) = try makeClient()
85 for _ in 0..<2 {
86 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
87 stub.enqueue(.init(status: 200, json: mirrorsJSON, match: "argv=list"))
88 }
89 let model = MirrorsViewModel(client: client, repoPath: "krz/gitbay")
90
91 #expect(await model.sync())
92 await model.remove(id: 2)
93
94 #expect(try writes(stub).map { $0["argv"] as? [String] } == [
95 ["repo", "mirror", "sync", "krz/gitbay"],
96 ["repo", "mirror", "remove", "krz/gitbay", "2"],
97 ])
98 }
99}
100
101@MainActor
102struct PageDomainsViewModelTests {
103
104 @Test func listsClaimsWithTheirState() async throws {
105 let (client, stub) = try makeClient()
106 stub.enqueue(.init(status: 200, json: domainsJSON))
107 let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay")
108 await model.load()
109
110 let domains = try #require(model.state.value)
111 #expect(domains.map(\.domain) == ["docs.example.test", "www.example.test", "old.example.test"])
112 #expect(domains[0].isPending && domains[0].verifiedAt == nil)
113 #expect(domains[1].isVerified && domains[1].verifiedAt != nil)
114 #expect(!domains[2].isPending && !domains[2].isVerified)
115 }
116
117 @Test func addReturnsTheDNSChallenge() async throws {
118 let (client, stub) = try makeClient()
119 stub.enqueue(.init(status: 200, json: """
120 {"protocol_version":1,"data":{"challenge_name":"_gitbay-challenge.docs.example.test",\
121 "challenge_value":"gitbay-domain-verify=abc","domain":"docs.example.test","state":"pending"},"exit_code":0}
122 """, match: "cmd"))
123 stub.enqueue(.init(status: 200, json: domainsJSON, match: "argv=list"))
124 let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay")
125
126 let challenge = try #require(await model.add(" Docs.Example.Test "))
127
128 #expect(challenge.name == "_gitbay-challenge.docs.example.test")
129 #expect(challenge.value == "gitbay-domain-verify=abc")
130 #expect(try writes(stub).first?["argv"] as? [String] == ["repo", "domain", "add", "krz/gitbay", "Docs.Example.Test"])
131 }
132
133 @Test func verifyReportsAMissingRecordVerbatim() async throws {
134 let (client, stub) = try makeClient()
135 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}"#))
136 let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay")
137
138 #expect(await model.verify("docs.example.test") == false)
139 #expect(model.actionError == #"_gitbay-challenge.docs.example.test does not carry the expected record "x""#)
140 }
141
142 @Test func aRefusedVerifyRefreshesTheList() async throws {
143 let (client, stub) = try makeClient()
144 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"))
145 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[],"exit_code":0}"#, match: "argv=list"))
146 let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay")
147
148 #expect(await model.verify("old.example.test") == false)
149
150 #expect(model.actionError == "the claim on old.example.test expired; run repo domain add again")
151 guard case .empty = model.state else {
152 Issue.record("expected the removed claim gone, got \(model.state)")
153 return
154 }
155 }
156
157 @Test func verifyAndRemoveNameTheDomain() async throws {
158 let (client, stub) = try makeClient()
159 for _ in 0..<2 {
160 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"domain":"d","state":"verified"},"exit_code":0}"#, match: "cmd"))
161 stub.enqueue(.init(status: 200, json: domainsJSON, match: "argv=list"))
162 }
163 let model = PageDomainsViewModel(client: client, repoPath: "krz/gitbay")
164
165 #expect(await model.verify("docs.example.test"))
166 await model.remove("www.example.test")
167
168 #expect(try writes(stub).map { $0["argv"] as? [String] } == [
169 ["repo", "domain", "verify", "krz/gitbay", "docs.example.test"],
170 ["repo", "domain", "remove", "krz/gitbay", "www.example.test"],
171 ])
172 }
173}