gitbayTests/AdminAccountTests.swift

v1.13.1
gitbay-ios/gitbayTests/AdminAccountTests.swift history · blame · raw

167 lines · 8003 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 posted(_ stub: StubProtocol.Box) throws -> [([String], String?)] {
 16    try stub.seen.filter { $0.method == "POST" }.map { seen in
 17        let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 18        return (try #require(body["argv"] as? [String]), body["stdin"] as? String)
 19    }
 20}
 21
 22private let showJSON = """
 23    {"protocol_version":1,"data":{"username":"other","state":"active","admin":true,\
 24    "created_at":"2026-10-14T07:49:15.505Z","keys":[{"fingerprint":"SHA256:a","algo":"ssh-ed25519",\
 25    "scope":"full","label":"other","created_at":"2026-10-14T07:49:15.505Z"}],\
 26    "emails":[{"address":"other@example.test","verified":true,"verified_by":"admin","primary":true}],\
 27    "pgp_keys":[],"orgs":[{"org":"acme","role":"admin"}],"repos":2,"repo_limit":0,"byte_limit":0,\
 28    "api_tokens":[{"name":"ci","scope":"read","created_at":"2026-10-14T07:49:15.505Z"}],"web_sessions":1}}
 29    """
 30
 31private let limitsJSON = """
 32    {"protocol_version":1,"data":{"user":"other","repos":10,"bytes":0,"orgs":0,"repos_owned":2,\
 33    "bytes_owned":4096,"orgs_created":0,"override":true}}
 34    """
 35
 36private let ok = #"{"protocol_version":1,"data":{},"exit_code":0}"#
 37
 38@MainActor
 39struct AdminAccountTests {
 40
 41    private func loaded() async throws -> (AdminAccountViewModel, StubProtocol.Box) {
 42        let (client, stub) = try makeClient()
 43        stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show"))
 44        stub.enqueue(.init(status: 200, json: limitsJSON, match: "cmd"))
 45        let model = AdminAccountViewModel(client: client, username: "other")
 46        await model.load()
 47        return (model, stub)
 48    }
 49
 50    @Test func anAccountLoadsWithItsLimits() async throws {
 51        let (model, _) = try await loaded()
 52        let loaded = try #require(model.state.value)
 53        #expect(loaded.detail.keys.first?.label == "other")
 54        #expect(loaded.detail.emails.first?.primary == true)
 55        #expect(loaded.detail.orgs.first?.org == "acme")
 56        #expect(loaded.detail.apiTokens.first?.name == "ci")
 57        #expect(loaded.detail.webSessions == 1)
 58        #expect(loaded.limits?.repos == 10)
 59        #expect(loaded.limits?.reposOwned == 2)
 60        #expect(loaded.limits?.override == true)
 61    }
 62
 63    @Test func limitFlagsLeaveEmptyFieldsOutAndScaleMegabytes() {
 64        #expect(OwnerLimits.flags(repos: "5", megabytes: "", orgs: "default")
 65                == ["--repos", "5", "--orgs", "default"])
 66        #expect(OwnerLimits.flags(repos: "", megabytes: "2", orgs: nil) == ["--bytes", "2000000"])
 67        #expect(OwnerLimits.flags(repos: "x", megabytes: "", orgs: nil) == nil)
 68        #expect(OwnerLimits.flags(repos: "-1", megabytes: "", orgs: nil) == nil)
 69        #expect(OwnerLimits.flags(repos: "", megabytes: "", orgs: "") == [])
 70        // A cap too large to hold in bytes is refused, not a crash.
 71        #expect(OwnerLimits.flags(repos: "", megabytes: "10000000000000", orgs: nil) == nil)
 72    }
 73
 74    @Test func settingLimitsSendsTheChangedCaps() async throws {
 75        let (model, stub) = try await loaded()
 76        stub.enqueue(.init(status: 200, json: limitsJSON, match: "cmd"))
 77        stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show"))
 78        stub.enqueue(.init(status: 200, json: limitsJSON, match: "cmd"))
 79
 80        #expect(await model.setLimits(repos: "20", megabytes: "", orgs: "0"))
 81
 82        // The first post is the load's read of the caps; the write follows.
 83        #expect(try posted(stub).map(\.0) == [["admin", "user", "limits", "other"],
 84                                                ["admin", "user", "limits", "other", "--repos", "20", "--orgs", "0"],
 85                                                ["admin", "user", "limits", "other"]])
 86    }
 87
 88    @Test func aMalformedCapIsRefusedBeforeAsking() async throws {
 89        let (model, stub) = try await loaded()
 90
 91        #expect(!(await model.setLimits(repos: "lots", megabytes: "", orgs: "")))
 92        #expect(try posted(stub).count == 1) // the load's read only
 93        #expect(model.actionError != nil)
 94    }
 95
 96    @Test func deletingSendsYes() async throws {
 97        let (model, stub) = try await loaded()
 98        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"deleted":"other"},"exit_code":0}"#, match: "cmd"))
 99
100        #expect(await model.delete())
101        #expect(try posted(stub).map(\.0).last == ["admin", "user", "delete", "other", "--yes"])
102    }
103}
104
105@MainActor
106struct AdminCreateInviteTests {
107
108    @Test func creatingSendsTheKeyOnStdin() async throws {
109        let (client, stub) = try makeClient()
110        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"user":"dee"},"exit_code":0}"#, match: "cmd"))
111        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#, match: "argv=list"))
112        let model = AdminUsersViewModel(client: client)
113
114        let made = await model.create(username: " dee ", email: "dee@example.test", verified: true, admin: false,
115                                      key: "ssh-ed25519 AAAA dee\n")
116
117        #expect(made)
118        let (argv, stdin) = try #require(try posted(stub).first)
119        #expect(argv == ["admin", "user", "create", "dee", "--email", "dee@example.test", "--verified", "--key", "-"])
120        #expect(stdin == "ssh-ed25519 AAAA dee")
121    }
122
123    @Test func noEmailMeansNoVerifiedFlagAndNoKeyMeansNoStdin() async throws {
124        let (client, stub) = try makeClient()
125        stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
126        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#, match: "argv=list"))
127        let model = AdminUsersViewModel(client: client)
128
129        _ = await model.create(username: "eve", email: "", verified: true, admin: true, key: "  ")
130
131        let (argv, stdin) = try #require(try posted(stub).first)
132        #expect(argv == ["admin", "user", "create", "eve", "--admin"])
133        #expect(stdin == nil)
134    }
135
136    @Test func anInviteThatCouldNotBeMailedHandsOverTheCode() async throws {
137        let (client, stub) = try makeClient()
138        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"email":"fay@example.test","mailed":false,"code":"abc123"},"exit_code":0}"#, match: "cmd"))
139        let model = AdminUsersViewModel(client: client)
140
141        let note = await model.invite(email: "fay@example.test")
142
143        #expect(try posted(stub).map(\.0) == [["admin", "invite", "--email", "fay@example.test"]])
144        #expect(note?.contains("abc123") == true)
145    }
146
147    @Test func aLimitsReadThatFailsSaysWhy() async throws {
148        let (client, stub) = try makeClient()
149        stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show"))
150        stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"a read-scoped token cannot run that","exit_code":4}"#, match: "cmd"))
151        let model = AdminAccountViewModel(client: client, username: "other")
152
153        await model.load()
154
155        #expect(model.state.value?.limits == nil)
156        #expect(model.state.value?.limitsError == "a read-scoped token cannot run that")
157    }
158
159    @Test func aDeleteWhoseRetryFindsNothingIsDone() async throws {
160        let (client, stub) = try makeClient()
161        stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"cleanup failed","exit_code":1}"#, match: "cmd"))
162        stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no user \"other\"","exit_code":3}"#, match: "cmd"))
163        let model = AdminAccountViewModel(client: client, username: "other")
164
165        #expect(await model.delete())
166    }
167}