a native ios client for gitbay

client ios swift

https://gitbay.org

gitbayTests/RepoManagementTests.swift

ui-smoke
gitbay-ios/gitbayTests/RepoManagementTests.swift history · blame · raw

269 lines · 11364 bytes

  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 argvOf(_ seen: StubProtocol.Seen) throws -> [String] {
 16    let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 17    return try #require(body["argv"] as? [String])
 18}
 19
 20private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
 21
 22private let settingsShowJSON = """
 23    {"protocol_version":1,"data":{"protected_branches":["main"],"require_approvals":1,\
 24    "require_resolved":true,"website":"https://gitbay.org"},"exit_code":0}
 25    """
 26
 27private let repoShowJSON = """
 28    {"protocol_version":1,"data":{"path":"krz/gitbay","description":"a forge",\
 29    "visibility":"public","default_branch":"main","topics":["git"]},"exit_code":0}
 30    """
 31
 32@MainActor
 33struct RepoSettingsViewModelTests {
 34
 35    private func loadedModel() async throws -> (RepoSettingsViewModel, StubProtocol.Box) {
 36        let (client, stub) = try makeClient()
 37        stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings"))
 38        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz"))
 39        let model = RepoSettingsViewModel(client: client, repoPath: "krz/gitbay")
 40        await model.load()
 41        return (model, stub)
 42    }
 43
 44    @Test func loadsSettingsAndDetailTogether() async throws {
 45        let (model, _) = try await loadedModel()
 46
 47        let loaded = try #require(model.state.value)
 48        #expect(loaded.settings.branches == ["main"])
 49        #expect(loaded.settings.requireApprovals == 1)
 50        #expect(loaded.settings.requireResolved == true)
 51        // Absent means default, not unknown.
 52        #expect(loaded.settings.requireChecks == nil)
 53        #expect(loaded.detail.topics == ["git"])
 54    }
 55
 56    @Test func aNonAdminGetsTheRefusalAsTheScreenState() async throws {
 57        let (client, stub) = try makeClient()
 58        stub.enqueue(.init(status: 403, json:
 59            #"{"protocol_version":1,"error":"admin access required","exit_code":4}"#,
 60            match: "argv=settings"))
 61        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz"))
 62        let model = RepoSettingsViewModel(client: client, repoPath: "krz/gitbay")
 63
 64        await model.load()
 65
 66        guard case .failed(let message) = model.state else {
 67            Issue.record("expected .failed, got \(model.state)")
 68            return
 69        }
 70        #expect(message == "admin access required")
 71    }
 72
 73    @Test func everyKnobSendsItsOwnCommand() async throws {
 74        let (model, stub) = try await loadedModel()
 75        for _ in 0..<12 {
 76            stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
 77            stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings"))
 78            stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz"))
 79        }
 80
 81        await model.setDescription("new text")
 82        await model.setWebsite("https://x.example")
 83        await model.setVisibility("private")
 84        await model.setGitDaemon(true)
 85        await model.protectBranch("release")
 86        await model.unprotectBranch("main")
 87        await model.setRequireApprovals(2)
 88        await model.setRequireResolved(false)
 89        await model.setRequireChecks(true)
 90        await model.setRequireSigned(true)
 91        await model.addTopic("ios")
 92        await model.removeTopic("git")
 93
 94        let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf)
 95        #expect(writes == [
 96            ["repo", "settings", "description", "krz/gitbay", "new text"],
 97            ["repo", "settings", "website", "krz/gitbay", "https://x.example"],
 98            ["repo", "settings", "visibility", "krz/gitbay", "private"],
 99            ["repo", "settings", "git-daemon", "krz/gitbay", "on"],
100            ["repo", "settings", "protect", "krz/gitbay", "release"],
101            ["repo", "settings", "unprotect", "krz/gitbay", "main"],
102            ["repo", "settings", "require-approvals", "krz/gitbay", "2"],
103            ["repo", "settings", "require-resolved", "krz/gitbay", "off"],
104            ["repo", "settings", "require-checks", "krz/gitbay", "on"],
105            ["repo", "settings", "require-signed", "krz/gitbay", "on"],
106            ["repo", "topics", "add", "krz/gitbay", "ios"],
107            ["repo", "topics", "remove", "krz/gitbay", "git"],
108        ])
109    }
110
111    @Test func aDeniedKnobSurfacesAndKeepsTheScreen() async throws {
112        let (model, stub) = try await loadedModel()
113        stub.enqueue(.init(status: 403, json:
114            #"{"protocol_version":1,"error":"only admins can change visibility","exit_code":4}"#,
115            match: "cmd"))
116
117        await model.setVisibility("private")
118
119        #expect(model.actionError == "only admins can change visibility")
120        #expect(model.state.value != nil)
121    }
122}
123
124@MainActor
125struct RepoPinArchiveTests {
126
127    private let dashboardPinnedJSON = """
128        {"protocol_version":1,"data":{"pinned":[{"path":"krz/gitbay","visibility":"public"}],\
129        "open_mrs":[],"assigned_issues":[],"builds":[]},"exit_code":0}
130        """
131    private let dashboardEmptyJSON = """
132        {"protocol_version":1,"data":{"pinned":[],"open_mrs":[],"assigned_issues":[],"builds":[]},\
133        "exit_code":0}
134        """
135    private let treeJSON = """
136        {"protocol_version":1,"data":{"path":"krz/gitbay","ref":"main","dir":"","entries":[]},\
137        "exit_code":0}
138        """
139
140    @Test func pinStateComesFromTheDashboardAggregate() async throws {
141        let (client, stub) = try makeClient()
142        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
143        stub.enqueue(.init(status: 200, json: dashboardPinnedJSON, match: "argv=dashboard"))
144        stub.enqueue(.init(status: 200, json: treeJSON, match: "argv=tree"))
145        let model = RepoDetailViewModel(client: client, path: "krz/gitbay")
146
147        await model.load()
148
149        #expect(model.isPinned == true)
150    }
151
152    @Test func unpinSendsTheCommandAndRefreshesState() async throws {
153        let (client, stub) = try makeClient()
154        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
155        stub.enqueue(.init(status: 200, json: dashboardPinnedJSON, match: "argv=dashboard"))
156        stub.enqueue(.init(status: 200, json: treeJSON, match: "argv=tree"))
157        let model = RepoDetailViewModel(client: client, path: "krz/gitbay")
158        await model.load()
159
160        stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
161        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
162        stub.enqueue(.init(status: 200, json: dashboardEmptyJSON, match: "argv=dashboard"))
163
164        await model.setPinned(false)
165
166        let write = try #require(stub.seen.first { $0.method == "POST" })
167        #expect(try argvOf(write) == ["repo", "unpin", "krz/gitbay"])
168        #expect(model.isPinned == false)
169    }
170
171    @Test func archiveSendsTheCommand() async throws {
172        let (client, stub) = try makeClient()
173        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
174        stub.enqueue(.init(status: 200, json: dashboardEmptyJSON, match: "argv=dashboard"))
175        stub.enqueue(.init(status: 200, json: treeJSON, match: "argv=tree"))
176        let model = RepoDetailViewModel(client: client, path: "krz/gitbay")
177        await model.load()
178
179        stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
180        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
181
182        await model.setArchived(true)
183
184        let write = try #require(stub.seen.first { $0.method == "POST" })
185        #expect(try argvOf(write) == ["repo", "archive", "krz/gitbay"])
186    }
187}
188
189@MainActor
190struct RepoCreateViewModelTests {
191
192    @Test func createSendsPathAndPrivateFlag() async throws {
193        let (client, stub) = try makeClient()
194        stub.enqueue(.init(status: 200, json:
195            #"{"protocol_version":1,"data":{"path":"cmc/notes","visibility":"private","ssh_url":"ssh://git@gitbay.org/cmc/notes.git"},"exit_code":0}"#))
196        let model = RepoCreateViewModel(client: client)
197
198        let path = await model.create(path: " cmc/notes ", isPrivate: true)
199
200        #expect(path == "cmc/notes")
201        let write = try #require(stub.seen.first)
202        #expect(try argvOf(write) == ["repo", "create", "cmc/notes", "--private"])
203    }
204
205    @Test func publicCreateOmitsTheFlag() async throws {
206        let (client, stub) = try makeClient()
207        stub.enqueue(.init(status: 200, json:
208            #"{"protocol_version":1,"data":{"path":"cmc/notes","visibility":"public","ssh_url":"x"},"exit_code":0}"#))
209        let model = RepoCreateViewModel(client: client)
210
211        _ = await model.create(path: "cmc/notes", isPrivate: false)
212
213        #expect(try argvOf(try #require(stub.seen.first)) == ["repo", "create", "cmc/notes"])
214    }
215
216    @Test func aRefusalSurfaces() async throws {
217        let (client, stub) = try makeClient()
218        stub.enqueue(.init(status: 403, json:
219            #"{"protocol_version":1,"error":"you cannot create repositories under krz","exit_code":4}"#))
220        let model = RepoCreateViewModel(client: client)
221
222        let path = await model.create(path: "krz/nope", isPrivate: false)
223
224        #expect(path == nil)
225        #expect(model.errorMessage == "you cannot create repositories under krz")
226    }
227}
228
229@MainActor
230struct BuildTriggerTests {
231
232    private let buildListJSON = """
233        {"protocol_version":1,"data":[\
234        {"number":3,"job":"ci","status":"success","sha":"65ba14e0000000000000",\
235        "ref":"refs/heads/main","created_at":"2026-08-20T10:00:00.000Z"}],"exit_code":0}
236        """
237
238    @Test func triggerSendsTheJobAndReloads() async throws {
239        let (client, stub) = try makeClient()
240        stub.enqueue(.init(status: 200, json: buildListJSON, match: "argv=build&argv=list"))
241        let model = BuildListViewModel(client: client, repoPath: "krz/gitbay")
242        await model.load()
243
244        stub.enqueue(.init(status: 200, json:
245            #"{"protocol_version":1,"data":{"build":4,"job":"ci","sha":"aa"},"exit_code":0}"#,
246            match: "cmd"))
247        stub.enqueue(.init(status: 200, json: buildListJSON, match: "argv=build&argv=list"))
248
249        await model.trigger(job: "ci")
250
251        let write = try #require(stub.seen.first { $0.method == "POST" })
252        #expect(try argvOf(write) == ["build", "trigger", "krz/gitbay", "ci"])
253        #expect(model.actionError == nil)
254    }
255
256    @Test func anUnknownJobSurfacesAsAnError() async throws {
257        let (client, stub) = try makeClient()
258        stub.enqueue(.init(status: 200, json: buildListJSON, match: "argv=build&argv=list"))
259        let model = BuildListViewModel(client: client, repoPath: "krz/gitbay")
260        await model.load()
261        stub.enqueue(.init(status: 404, json:
262            #"{"protocol_version":1,"error":"no job \"deploy\" in .gitbay/ci.yml","exit_code":3}"#,
263            match: "cmd"))
264
265        await model.trigger(job: "deploy")
266
267        #expect(model.actionError == #"no job "deploy" in .gitbay/ci.yml"#)
268    }
269}