gitbayTests/TokenTests.swift
106 lines · 4806 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]] {
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])
19 }
20}
21
22private let listJSON = """
23 {"protocol_version":1,"data":[\
24 {"name":"iphone","scope":"full","created_at":"2026-09-01T10:00:00.000Z","last_used_at":"2026-10-14T07:00:00Z"},\
25 {"name":"ci","scope":"read","created_at":"2026-09-02T10:00:00.000Z","expires_at":"2026-01-01T00:00:00Z","created_by":"iphone"}\
26 ],"exit_code":0}
27 """
28
29@MainActor
30struct TokenTests {
31
32 private func loaded() async throws -> (TokensViewModel, StubProtocol.Box) {
33 let (client, stub) = try makeClient()
34 stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
35 let model = TokensViewModel(client: client)
36 await model.load()
37 return (model, stub)
38 }
39
40 @Test func tokensListWithUseExpiryAndWhoMadeThem() async throws {
41 let (model, _) = try await loaded()
42 let tokens = try #require(model.state.value)
43 #expect(tokens.map(\.name) == ["iphone", "ci"])
44 #expect(tokens[0].lastUsedAt != nil)
45 #expect(tokens[1].createdBy == "iphone")
46 #expect(tokens[1].isExpired(at: .now))
47 }
48
49 @Test func mintingReturnsTheTokenOnceAndReloads() async throws {
50 let (model, stub) = try await loaded()
51 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"name":"laptop","scope":"read","token":"gbt_secret"},"exit_code":0}"#, match: "cmd"))
52 stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
53
54 let minted = await model.create(name: " laptop ", scope: "read", ttl: "30d")
55
56 #expect(minted == "gbt_secret")
57 #expect(try posted(stub) == [["token", "create", "--name", "laptop", "--scope", "read", "--ttl", "30d"]])
58 }
59
60 @Test func aTokenThatNeverExpiresSendsNoTTL() async throws {
61 let (model, stub) = try await loaded()
62 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"name":"x","scope":"full","token":"t"},"exit_code":0}"#, match: "cmd"))
63 stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
64
65 _ = await model.create(name: "x", scope: "full", ttl: nil)
66
67 #expect(try posted(stub) == [["token", "create", "--name", "x", "--scope", "full"]])
68 }
69
70 /// An expiring token cannot mint one; the server says so, and the
71 /// app's sentence for that refusal says what to do instead.
72 @Test func anExpiringSessionIsToldWhy() async throws {
73 let (model, stub) = try await loaded()
74 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"))
75
76 #expect(await model.create(name: "x", scope: "full", ttl: nil) == nil)
77 #expect(model.actionError?.contains("without an expiry") == true)
78 }
79
80 @Test func revokingCanTakeWhatTheTokenCreated() async throws {
81 let (model, stub) = try await loaded()
82 stub.enqueue(.init(status: 200, json: """
83 {"protocol_version":1,"data":{"revoked":"iphone","created":{"tokens":["ci"],"keys":["SHA256:k"]},"created_revoked":true},"exit_code":0}
84 """, match: "cmd"))
85 stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
86 let token = try #require(model.state.value?.first)
87
88 let created = await model.revoke(token, withCreated: true)
89
90 #expect(try posted(stub) == [["token", "revoke", "iphone", "--created"]])
91 #expect(created?.tokens == ["ci"])
92 #expect(created?.keys == ["SHA256:k"])
93 }
94
95 @Test func revokingAloneLeavesWhatItCreated() async throws {
96 let (model, stub) = try await loaded()
97 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"))
98 stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
99 let token = try #require(model.state.value?.last)
100
101 let created = await model.revoke(token, withCreated: false)
102
103 #expect(try posted(stub) == [["token", "revoke", "ci"]])
104 #expect(created?.isEmpty == true)
105 }
106}