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]] { 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":[\ {"name":"iphone","scope":"full","created_at":"2026-09-01T10:00:00.000Z","last_used_at":"2026-10-14T07:00:00Z"},\ {"name":"ci","scope":"read","created_at":"2026-09-02T10:00:00.000Z","expires_at":"2026-01-01T00:00:00Z","created_by":"iphone"}\ ],"exit_code":0} """ @MainActor struct TokenTests { private func loaded() async throws -> (TokensViewModel, StubProtocol.Box) { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) let model = TokensViewModel(client: client) await model.load() return (model, stub) } @Test func tokensListWithUseExpiryAndWhoMadeThem() async throws { let (model, _) = try await loaded() let tokens = try #require(model.state.value) #expect(tokens.map(\.name) == ["iphone", "ci"]) #expect(tokens[0].lastUsedAt != nil) #expect(tokens[1].createdBy == "iphone") #expect(tokens[1].isExpired(at: .now)) } @Test func mintingReturnsTheTokenOnceAndReloads() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"name":"laptop","scope":"read","token":"gbt_secret"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) let minted = await model.create(name: " laptop ", scope: "read", ttl: "30d") #expect(minted == "gbt_secret") #expect(try posted(stub) == [["token", "create", "--name", "laptop", "--scope", "read", "--ttl", "30d"]]) } @Test func aTokenThatNeverExpiresSendsNoTTL() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"name":"x","scope":"full","token":"t"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) _ = await model.create(name: "x", scope: "full", ttl: nil) #expect(try posted(stub) == [["token", "create", "--name", "x", "--scope", "full"]]) } /// An expiring token cannot mint one; the server says so, and the /// app's sentence for that refusal says what to do instead. @Test func anExpiringSessionIsToldWhy() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"token create creates a credential, and the one this request came with expires; use a token or key without an expiry","exit_code":4}"#, match: "cmd")) #expect(await model.create(name: "x", scope: "full", ttl: nil) == nil) #expect(model.actionError?.contains("without an expiry") == true) } @Test func revokingCanTakeWhatTheTokenCreated() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"revoked":"iphone","created":{"tokens":["ci"],"keys":["SHA256:k"]},"created_revoked":true},"exit_code":0} """, match: "cmd")) stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) let token = try #require(model.state.value?.first) let created = await model.revoke(token, withCreated: true) #expect(try posted(stub) == [["token", "revoke", "iphone", "--created"]]) #expect(created?.tokens == ["ci"]) #expect(created?.keys == ["SHA256:k"]) } @Test func revokingAloneLeavesWhatItCreated() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"revoked":"ci","created":{"tokens":null,"keys":null},"created_revoked":false},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list")) let token = try #require(model.state.value?.last) let created = await model.revoke(token, withCreated: false) #expect(try posted(stub) == [["token", "revoke", "ci"]]) #expect(created?.isEmpty == true) } }