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 argv(_ url: URL) -> [String] { URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.compactMap(\.value) ?? [] } private func posted(_ stub: StubProtocol.Box) throws -> [[String]] { try stub.seen.filter { $0.method == "POST" }.map { seen in let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return try #require(body["argv"] as? [String]) } } private let listJSON = """ {"protocol_version":1,"data":{"items":[\ {"path":"sim/app","visibility":"public","created_at":"2026-10-14T04:19:38.964Z","last_push":"2026-10-14T06:04:58.461Z","bytes":38159},\ {"path":"sim/old","visibility":"private","archived":true,"created_at":"2026-10-14T04:19:38.964Z","bytes":10}]}} """ @MainActor struct AdminRepoTests { @Test func theListIsFilteredByOwnerAndVisibility() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listJSON)) let model = AdminReposViewModel(client: client) model.owner = " sim " model.visibility = "private" await model.load() #expect(argv(try #require(stub.seen.first).url) == ["admin", "repo", "list", "--owner", "sim", "--visibility", "private", "--limit", "50"]) let repos = try #require(model.list.state.value) #expect(repos.map(\.isArchived) == [false, true]) #expect(repos.last?.lastPush == nil) } @Test func eachActionSendsItsCommand() { #expect(AdminRepoAction.archive.argv("a/b") == ["admin", "repo", "archive", "a/b"]) #expect(AdminRepoAction.unarchive.argv("a/b") == ["admin", "repo", "unarchive", "a/b"]) #expect(AdminRepoAction.makePrivate.argv("a/b") == ["admin", "repo", "visibility", "a/b", "private"]) #expect(AdminRepoAction.reindex.argv("a/b") == ["admin", "symbols", "reindex", "a/b"]) #expect(AdminRepoAction.delete.argv("a/b") == ["admin", "repo", "delete", "a/b", "--yes"]) } @Test func aReindexSaysItIsQueued() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"repo":"sim/app","state":"queued"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) let model = AdminReposViewModel(client: client) await model.load() let repo = try #require(model.list.state.value?.first) await model.perform(.reindex, on: repo) #expect(try posted(stub) == [["admin", "symbols", "reindex", "sim/app"]]) #expect(model.notice?.contains("queued") == true) } } @MainActor struct AdminRepoDeleteTests { @Test func aDeleteWhoseRetryFindsNothingIsDoneAndAnnounced() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"cleanup failed","exit_code":1}"#, match: "cmd")) stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no repository sim/app","exit_code":3}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) let model = AdminReposViewModel(client: client) await model.load() let repo = try #require(model.list.state.value?.first) let announced = NotificationCenter.default.notifications(named: .gitbayRepositoryChanged) async let first = announced.first { _ in true } await model.perform(.delete, on: repo) #expect(model.actionError == nil) #expect((await first)?.object as? String == "sim/app") // The list stayed on screen through the reload. #expect(model.list.state.value != nil) } } @MainActor struct AdminOrgLimitsTests { @Test func anOrgsLimitsLoadAndChange() async throws { let (client, stub) = try makeClient() let json = #"{"protocol_version":1,"data":{"org":"acme","repos":0,"bytes":0,"repos_owned":1,"bytes_owned":2,"override":false}}"# stub.enqueue(.init(status: 200, json: json, match: "cmd")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"org":"acme","repos":5,"bytes":0,"repos_owned":1,"bytes_owned":2,"override":true}}"#, match: "cmd")) let model = AdminOrgLimitsViewModel(client: client) await model.load(org: " acme ") #expect(model.limits?.reposOwned == 1) #expect(model.limits?.orgs == nil) #expect(await model.set(repos: "5", megabytes: "")) #expect(try posted(stub) == [["admin", "org", "limits", "acme"], ["admin", "org", "limits", "acme", "--repos", "5"]]) #expect(model.limits?.repos == 5) } @Test func anUnknownOrgSaysSo() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no organization \"nope\"","exit_code":3}"#, match: "cmd")) let model = AdminOrgLimitsViewModel(client: client) await model.load(org: "nope") #expect(model.limits == nil) #expect(model.actionError == #"no organization "nope""#) } }