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}