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 argvOf(_ seen: StubProtocol.Seen) throws -> [String] { let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return try #require(body["argv"] as? [String]) } private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# private let settingsShowJSON = """ {"protocol_version":1,"data":{"protected_branches":["main"],"require_approvals":1,\ "require_resolved":true,"website":"https://gitbay.org"},"exit_code":0} """ private let repoShowJSON = """ {"protocol_version":1,"data":{"path":"krz/gitbay","description":"a forge",\ "visibility":"public","default_branch":"main","topics":["git"]},"exit_code":0} """ @MainActor struct RepoSettingsViewModelTests { private func loadedModel() async throws -> (RepoSettingsViewModel, StubProtocol.Box) { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings")) stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz")) let model = RepoSettingsViewModel(client: client, repoPath: "krz/gitbay") await model.load() return (model, stub) } @Test func loadsSettingsAndDetailTogether() async throws { let (model, _) = try await loadedModel() let loaded = try #require(model.state.value) #expect(loaded.settings.branches == ["main"]) #expect(loaded.settings.requireApprovals == 1) #expect(loaded.settings.requireResolved == true) // Absent means default, not unknown. #expect(loaded.settings.requireChecks == nil) #expect(loaded.detail.topics == ["git"]) } @Test func aNonAdminGetsTheRefusalAsTheScreenState() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"admin access required","exit_code":4}"#, match: "argv=settings")) stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz")) let model = RepoSettingsViewModel(client: client, repoPath: "krz/gitbay") await model.load() guard case .failed(let message) = model.state else { Issue.record("expected .failed, got \(model.state)") return } #expect(message == "admin access required") } @Test func everyKnobSendsItsOwnCommand() async throws { let (model, stub) = try await loadedModel() for _ in 0..<12 { stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: settingsShowJSON, match: "argv=settings")) stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show&argv=krz")) } await model.setDescription("new text") await model.setWebsite("https://x.example") await model.setVisibility("private") await model.setGitDaemon(true) await model.protectBranch("release") await model.unprotectBranch("main") await model.setRequireApprovals(2) await model.setRequireResolved(false) await model.setRequireChecks(true) await model.setRequireSigned(true) await model.addTopic("ios") await model.removeTopic("git") let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf) #expect(writes == [ ["repo", "settings", "description", "krz/gitbay", "new text"], ["repo", "settings", "website", "krz/gitbay", "https://x.example"], ["repo", "settings", "visibility", "krz/gitbay", "private"], ["repo", "settings", "git-daemon", "krz/gitbay", "on"], ["repo", "settings", "protect", "krz/gitbay", "release"], ["repo", "settings", "unprotect", "krz/gitbay", "main"], ["repo", "settings", "require-approvals", "krz/gitbay", "2"], ["repo", "settings", "require-resolved", "krz/gitbay", "off"], ["repo", "settings", "require-checks", "krz/gitbay", "on"], ["repo", "settings", "require-signed", "krz/gitbay", "on"], ["repo", "topics", "add", "krz/gitbay", "ios"], ["repo", "topics", "remove", "krz/gitbay", "git"], ]) } @Test func aDeniedKnobSurfacesAndKeepsTheScreen() async throws { let (model, stub) = try await loadedModel() stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"only admins can change visibility","exit_code":4}"#, match: "cmd")) await model.setVisibility("private") #expect(model.actionError == "only admins can change visibility") #expect(model.state.value != nil) } } @MainActor struct RepoPinArchiveTests { private let dashboardPinnedJSON = """ {"protocol_version":1,"data":{"pinned":[{"path":"krz/gitbay","visibility":"public"}],\ "open_mrs":[],"assigned_issues":[],"builds":[]},"exit_code":0} """ private let dashboardEmptyJSON = """ {"protocol_version":1,"data":{"pinned":[],"open_mrs":[],"assigned_issues":[],"builds":[]},\ "exit_code":0} """ private let treeJSON = """ {"protocol_version":1,"data":{"path":"krz/gitbay","ref":"main","dir":"","entries":[]},\ "exit_code":0} """ @Test func pinStateComesFromTheDashboardAggregate() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show")) stub.enqueue(.init(status: 200, json: dashboardPinnedJSON, match: "argv=dashboard")) stub.enqueue(.init(status: 200, json: treeJSON, match: "argv=tree")) let model = RepoDetailViewModel(client: client, path: "krz/gitbay") await model.load() #expect(model.isPinned == true) } @Test func unpinSendsTheCommandAndRefreshesState() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show")) stub.enqueue(.init(status: 200, json: dashboardPinnedJSON, match: "argv=dashboard")) stub.enqueue(.init(status: 200, json: treeJSON, match: "argv=tree")) let model = RepoDetailViewModel(client: client, path: "krz/gitbay") await model.load() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show")) stub.enqueue(.init(status: 200, json: dashboardEmptyJSON, match: "argv=dashboard")) await model.setPinned(false) let write = try #require(stub.seen.first { $0.method == "POST" }) #expect(try argvOf(write) == ["repo", "unpin", "krz/gitbay"]) #expect(model.isPinned == false) } @Test func archiveSendsTheCommand() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show")) stub.enqueue(.init(status: 200, json: dashboardEmptyJSON, match: "argv=dashboard")) stub.enqueue(.init(status: 200, json: treeJSON, match: "argv=tree")) let model = RepoDetailViewModel(client: client, path: "krz/gitbay") await model.load() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show")) await model.setArchived(true) let write = try #require(stub.seen.first { $0.method == "POST" }) #expect(try argvOf(write) == ["repo", "archive", "krz/gitbay"]) } } @MainActor struct RepoCreateViewModelTests { @Test func createSendsPathAndPrivateFlag() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"path":"cmc/notes","visibility":"private","ssh_url":"ssh://git@gitbay.org/cmc/notes.git"},"exit_code":0}"#)) let model = RepoCreateViewModel(client: client) let path = await model.create(path: " cmc/notes ", isPrivate: true) #expect(path == "cmc/notes") let write = try #require(stub.seen.first) #expect(try argvOf(write) == ["repo", "create", "cmc/notes", "--private"]) } @Test func publicCreateOmitsTheFlag() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"path":"cmc/notes","visibility":"public","ssh_url":"x"},"exit_code":0}"#)) let model = RepoCreateViewModel(client: client) _ = await model.create(path: "cmc/notes", isPrivate: false) #expect(try argvOf(try #require(stub.seen.first)) == ["repo", "create", "cmc/notes"]) } @Test func aRefusalSurfaces() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"you cannot create repositories under krz","exit_code":4}"#)) let model = RepoCreateViewModel(client: client) let path = await model.create(path: "krz/nope", isPrivate: false) #expect(path == nil) #expect(model.errorMessage == "you cannot create repositories under krz") } } @MainActor struct BuildTriggerTests { private let buildListJSON = """ {"protocol_version":1,"data":[\ {"number":3,"job":"ci","status":"success","sha":"65ba14e0000000000000",\ "ref":"refs/heads/main","created_at":"2026-08-20T10:00:00.000Z"}],"exit_code":0} """ @Test func triggerSendsTheJobAndReloads() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: buildListJSON, match: "argv=build&argv=list")) let model = BuildListViewModel(client: client, repoPath: "krz/gitbay") await model.load() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"build":4,"job":"ci","sha":"aa"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: buildListJSON, match: "argv=build&argv=list")) await model.trigger(job: "ci") let write = try #require(stub.seen.first { $0.method == "POST" }) #expect(try argvOf(write) == ["build", "trigger", "krz/gitbay", "ci"]) #expect(model.actionError == nil) } @Test func anUnknownJobSurfacesAsAnError() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: buildListJSON, match: "argv=build&argv=list")) let model = BuildListViewModel(client: client, repoPath: "krz/gitbay") await model.load() stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no job \"deploy\" in .gitbay/ci.yml","exit_code":3}"#, match: "cmd")) await model.trigger(job: "deploy") #expect(model.actionError == #"no job "deploy" in .gitbay/ci.yml"#) } }