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 posted(_ stub: StubProtocol.Box) throws -> [([String], 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]), body["stdin"] as? String) } } private let showJSON = """ {"protocol_version":1,"data":{"username":"other","state":"active","admin":true,\ "created_at":"2026-10-14T07:49:15.505Z","keys":[{"fingerprint":"SHA256:a","algo":"ssh-ed25519",\ "scope":"full","label":"other","created_at":"2026-10-14T07:49:15.505Z"}],\ "emails":[{"address":"other@example.test","verified":true,"verified_by":"admin","primary":true}],\ "pgp_keys":[],"orgs":[{"org":"acme","role":"admin"}],"repos":2,"repo_limit":0,"byte_limit":0,\ "api_tokens":[{"name":"ci","scope":"read","created_at":"2026-10-14T07:49:15.505Z"}],"web_sessions":1}} """ private let limitsJSON = """ {"protocol_version":1,"data":{"user":"other","repos":10,"bytes":0,"orgs":0,"repos_owned":2,\ "bytes_owned":4096,"orgs_created":0,"override":true}} """ private let ok = #"{"protocol_version":1,"data":{},"exit_code":0}"# @MainActor struct AdminAccountTests { private func loaded() async throws -> (AdminAccountViewModel, StubProtocol.Box) { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show")) stub.enqueue(.init(status: 200, json: limitsJSON, match: "cmd")) let model = AdminAccountViewModel(client: client, username: "other") await model.load() return (model, stub) } @Test func anAccountLoadsWithItsLimits() async throws { let (model, _) = try await loaded() let loaded = try #require(model.state.value) #expect(loaded.detail.keys.first?.label == "other") #expect(loaded.detail.emails.first?.primary == true) #expect(loaded.detail.orgs.first?.org == "acme") #expect(loaded.detail.apiTokens.first?.name == "ci") #expect(loaded.detail.webSessions == 1) #expect(loaded.limits?.repos == 10) #expect(loaded.limits?.reposOwned == 2) #expect(loaded.limits?.override == true) } @Test func limitFlagsLeaveEmptyFieldsOutAndScaleMegabytes() { #expect(OwnerLimits.flags(repos: "5", megabytes: "", orgs: "default") == ["--repos", "5", "--orgs", "default"]) #expect(OwnerLimits.flags(repos: "", megabytes: "2", orgs: nil) == ["--bytes", "2000000"]) #expect(OwnerLimits.flags(repos: "x", megabytes: "", orgs: nil) == nil) #expect(OwnerLimits.flags(repos: "-1", megabytes: "", orgs: nil) == nil) #expect(OwnerLimits.flags(repos: "", megabytes: "", orgs: "") == []) // A cap too large to hold in bytes is refused, not a crash. #expect(OwnerLimits.flags(repos: "", megabytes: "10000000000000", orgs: nil) == nil) } @Test func settingLimitsSendsTheChangedCaps() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: limitsJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show")) stub.enqueue(.init(status: 200, json: limitsJSON, match: "cmd")) #expect(await model.setLimits(repos: "20", megabytes: "", orgs: "0")) // The first post is the load's read of the caps; the write follows. #expect(try posted(stub).map(\.0) == [["admin", "user", "limits", "other"], ["admin", "user", "limits", "other", "--repos", "20", "--orgs", "0"], ["admin", "user", "limits", "other"]]) } @Test func aMalformedCapIsRefusedBeforeAsking() async throws { let (model, stub) = try await loaded() #expect(!(await model.setLimits(repos: "lots", megabytes: "", orgs: ""))) #expect(try posted(stub).count == 1) // the load's read only #expect(model.actionError != nil) } @Test func deletingSendsYes() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"deleted":"other"},"exit_code":0}"#, match: "cmd")) #expect(await model.delete()) #expect(try posted(stub).map(\.0).last == ["admin", "user", "delete", "other", "--yes"]) } } @MainActor struct AdminCreateInviteTests { @Test func creatingSendsTheKeyOnStdin() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"user":"dee"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#, match: "argv=list")) let model = AdminUsersViewModel(client: client) let made = await model.create(username: " dee ", email: "dee@example.test", verified: true, admin: false, key: "ssh-ed25519 AAAA dee\n") #expect(made) let (argv, stdin) = try #require(try posted(stub).first) #expect(argv == ["admin", "user", "create", "dee", "--email", "dee@example.test", "--verified", "--key", "-"]) #expect(stdin == "ssh-ed25519 AAAA dee") } @Test func noEmailMeansNoVerifiedFlagAndNoKeyMeansNoStdin() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: ok, match: "cmd")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#, match: "argv=list")) let model = AdminUsersViewModel(client: client) _ = await model.create(username: "eve", email: "", verified: true, admin: true, key: " ") let (argv, stdin) = try #require(try posted(stub).first) #expect(argv == ["admin", "user", "create", "eve", "--admin"]) #expect(stdin == nil) } @Test func anInviteThatCouldNotBeMailedHandsOverTheCode() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"email":"fay@example.test","mailed":false,"code":"abc123"},"exit_code":0}"#, match: "cmd")) let model = AdminUsersViewModel(client: client) let note = await model.invite(email: "fay@example.test") #expect(try posted(stub).map(\.0) == [["admin", "invite", "--email", "fay@example.test"]]) #expect(note?.contains("abc123") == true) } @Test func aLimitsReadThatFailsSaysWhy() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show")) stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"a read-scoped token cannot run that","exit_code":4}"#, match: "cmd")) let model = AdminAccountViewModel(client: client, username: "other") await model.load() #expect(model.state.value?.limits == nil) #expect(model.state.value?.limitsError == "a read-scoped token cannot run that") } @Test func aDeleteWhoseRetryFindsNothingIsDone() async throws { let (client, stub) = try makeClient() 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 user \"other\"","exit_code":3}"#, match: "cmd")) let model = AdminAccountViewModel(client: client, username: "other") #expect(await model.delete()) } }