a native ios client for gitbay

client ios swift

https://gitbay.org

gitbayTests/AccountTests.swift

122 lines · 5341 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 argvOf(_ seen: StubProtocol.Seen) throws -> ([String], String?) {
 16    let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 17    return (try #require(body["argv"] as? [String]), body["stdin"] as? String)
 18}
 19
 20private let keysJSON = """
 21    {"protocol_version":1,"data":[\
 22    {"fingerprint":"SHA256:15jrWGl3s3BB1CeG0z9TfGnyf35l8lcBWoYfA0+IJbY","algo":"ssh-ed25519","scope":"full"}\
 23    ],"exit_code":0}
 24    """
 25private let pgpJSON = """
 26    {"protocol_version":1,"data":[\
 27    {"fingerprint":"3917973fb159bbb86194538569451a517ac0cb37",\
 28    "emails":"[\\"hello@cleberg.net\\"]"}],"exit_code":0}
 29    """
 30private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
 31private let orgListJSON = """
 32    {"protocol_version":1,"data":[{"org":"krz","role":"admin"}],"exit_code":0}
 33    """
 34
 35@MainActor
 36struct AccountViewModelTests {
 37
 38    private func loadedModel() async throws -> (AccountViewModel, StubProtocol.Box) {
 39        let (client, stub) = try makeClient()
 40        stub.enqueue(.init(status: 200, json: keysJSON, match: "argv=keys"))
 41        stub.enqueue(.init(status: 200, json: pgpJSON, match: "argv=pgp"))
 42        stub.enqueue(.init(status: 200, json: orgListJSON, match: "argv=org"))
 43        let model = AccountViewModel(client: client)
 44        await model.load()
 45        return (model, stub)
 46    }
 47
 48    @Test func loadsBothKeyListsAndDecodesNestedEmails() async throws {
 49        let (model, _) = try await loadedModel()
 50
 51        let loaded = try #require(model.state.value)
 52        #expect(loaded.sshKeys.first?.algo == "ssh-ed25519")
 53        // The UID list is JSON-encoded inside the JSON field.
 54        #expect(loaded.pgpKeys.first?.emailList == ["hello@cleberg.net"])
 55    }
 56
 57    @Test func sshKeyTravelsAsRawStdinWithScope() async throws {
 58        let (model, stub) = try await loadedModel()
 59        stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
 60        stub.enqueue(.init(status: 200, json: keysJSON, match: "argv=keys"))
 61        stub.enqueue(.init(status: 200, json: pgpJSON, match: "argv=pgp"))
 62        stub.enqueue(.init(status: 200, json: orgListJSON, match: "argv=org"))
 63
 64        await model.addSSHKey("ssh-ed25519 AAAAC3Nza phone\n", scope: "git")
 65
 66        let (argv, stdin) = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
 67        // No --file - here: keys add reads bare stdin.
 68        #expect(argv == ["keys", "add", "--scope", "git"])
 69        #expect(stdin == "ssh-ed25519 AAAAC3Nza phone")
 70    }
 71
 72    @Test func removalsTargetTheFingerprint() async throws {
 73        let (model, stub) = try await loadedModel()
 74        for _ in 0..<2 {
 75            stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
 76            stub.enqueue(.init(status: 200, json: keysJSON, match: "argv=keys"))
 77            stub.enqueue(.init(status: 200, json: pgpJSON, match: "argv=pgp"))
 78            stub.enqueue(.init(status: 200, json: orgListJSON, match: "argv=org"))
 79        }
 80        let loaded = try #require(model.state.value)
 81
 82        await model.removeSSHKey(loaded.sshKeys[0])
 83        await model.removePGPKey(loaded.pgpKeys[0])
 84
 85        let writes = try stub.seen.filter { $0.method == "POST" }.map { try argvOf($0).0 }
 86        #expect(writes[0] == ["keys", "remove", "SHA256:15jrWGl3s3BB1CeG0z9TfGnyf35l8lcBWoYfA0+IJbY"])
 87        #expect(writes[1] == ["pgp", "remove", "3917973fb159bbb86194538569451a517ac0cb37"])
 88    }
 89
 90    @Test func emailAddAndVerifySendTheirCommands() async throws {
 91        let (model, stub) = try await loadedModel()
 92        for _ in 0..<2 {
 93            stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
 94            stub.enqueue(.init(status: 200, json: keysJSON, match: "argv=keys"))
 95            stub.enqueue(.init(status: 200, json: pgpJSON, match: "argv=pgp"))
 96            stub.enqueue(.init(status: 200, json: orgListJSON, match: "argv=org"))
 97        }
 98
 99        await model.addEmail(" claude@cleberg.net ")
100        #expect(model.notice?.contains("on its way") == true)
101        await model.verifyEmail(code: "123456")
102        #expect(model.notice == "Email verified.")
103
104        let writes = try stub.seen.filter { $0.method == "POST" }.map { try argvOf($0).0 }
105        #expect(writes[0] == ["email", "add", "claude@cleberg.net"])
106        #expect(writes[1] == ["email", "verify", "123456"])
107    }
108
109    @Test func contentValidationUsageErrorsSurfaceVerbatimHere() async throws {
110        let (model, stub) = try await loadedModel()
111        stub.enqueue(.init(status: 400, json:
112            #"{"protocol_version":1,"error":"not a valid public key in authorized_keys format: illegal base64","exit_code":2}"#,
113            match: "cmd"))
114
115        await model.addSSHKey("garbage", scope: "full")
116
117        // Exit 2 stays generic app-wide; on this screen it validates
118        // pasted content and the message is for the person.
119        #expect(model.actionError ==
120            "not a valid public key in authorized_keys format: illegal base64")
121    }
122}