gitbayTests/TokenTests.swift

v1.14.1
gitbay-ios/gitbayTests/TokenTests.swift history · blame · raw

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}