gitbayTests/RepoCredentialTests.swift
160 lines · 7550 bytes
14 symbols in this file
makeClientbodyBuildSecretsViewModelTestslistsSecretNamesnoSecretsIsAnEmptyStatesetSendsTheValueOnStdinNeverInArgvaRefusedNameSaysWhyaFailedWriteStillRefreshesTheListaMultiLineValueKeepsItsNewlinesremoveNamesTheSecretDeployKeysViewModelTestslistsKeysWithModeAndDatesaddSendsTheKeyOnStdinWithModeAndLifetimeremoveNamesTheFingerprint
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 body(_ seen: StubProtocol.Seen) throws -> [String: Any] {
16 try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
17}
18
19private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
20private let secretsJSON = #"{"protocol_version":1,"data":["DEPLOY_TOKEN","NPM_TOKEN"],"exit_code":0}"#
21private let deployKeysJSON = """
22 {"protocol_version":1,"data":[\
23 {"fingerprint":"SHA256:abc","algo":"ssh-ed25519","mode":"rw","label":"ci@box","last_used_at":"2026-10-10T08:00:00.000Z"},\
24 {"fingerprint":"SHA256:def","algo":"ssh-ed25519","mode":"ro","label":"","expires_at":"2026-11-10T08:00:00Z"}\
25 ],"exit_code":0}
26 """
27
28@MainActor
29struct BuildSecretsViewModelTests {
30
31 @Test func listsSecretNames() async throws {
32 let (client, stub) = try makeClient()
33 stub.enqueue(.init(status: 200, json: secretsJSON))
34 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
35 await model.load()
36 #expect(model.state.value == ["DEPLOY_TOKEN", "NPM_TOKEN"])
37 #expect(stub.seen.first?.url.query() == "argv=repo&argv=secret&argv=list&argv=krz/gitbay")
38 }
39
40 @Test func noSecretsIsAnEmptyState() async throws {
41 let (client, stub) = try makeClient()
42 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":null,"exit_code":0}"#))
43 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
44 await model.load()
45 guard case .empty = model.state else {
46 Issue.record("expected .empty, got \(model.state)")
47 return
48 }
49 }
50
51 @Test func setSendsTheValueOnStdinNeverInArgv() async throws {
52 let (client, stub) = try makeClient()
53 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"secret":"DEPLOY_TOKEN"},"exit_code":0}"#, match: "cmd"))
54 stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list"))
55 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
56
57 #expect(await model.set(name: " DEPLOY_TOKEN ", value: "s3cret"))
58
59 let sent = try body(try #require(stub.seen.first))
60 #expect(sent["argv"] as? [String] == ["repo", "secret", "set", "krz/gitbay", "DEPLOY_TOKEN"])
61 #expect(sent["stdin"] as? String == "s3cret")
62 #expect(model.state.value == ["DEPLOY_TOKEN", "NPM_TOKEN"])
63 }
64
65 @Test func aRefusedNameSaysWhy() async throws {
66 let (client, stub) = try makeClient()
67 stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"secret names are env-var shaped: uppercase letters, digits, _","exit_code":2}"#))
68 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
69
70 #expect(await model.set(name: "lower", value: "x") == false)
71 #expect(model.actionError == "secret names are env-var shaped: uppercase letters, digits, _")
72 }
73
74 @Test func aFailedWriteStillRefreshesTheList() async throws {
75 let (client, stub) = try makeClient()
76 stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no secret NPM_TOKEN on krz/gitbay","exit_code":3}"#, match: "cmd"))
77 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":["DEPLOY_TOKEN"],"exit_code":0}"#, match: "argv=list"))
78 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
79
80 await model.remove("NPM_TOKEN")
81
82 #expect(model.actionError == "no secret NPM_TOKEN on krz/gitbay")
83 #expect(model.state.value == ["DEPLOY_TOKEN"])
84 }
85
86 @Test func aMultiLineValueKeepsItsNewlines() async throws {
87 let (client, stub) = try makeClient()
88 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
89 stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list"))
90 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
91 let key = "-----BEGIN KEY-----\nabc\n-----END KEY-----\n"
92
93 #expect(await model.set(name: "BOT_SSH_KEY", value: key))
94
95 #expect(try body(try #require(stub.seen.first))["stdin"] as? String == key)
96 }
97
98 @Test func removeNamesTheSecret() async throws {
99 let (client, stub) = try makeClient()
100 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
101 stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list"))
102 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
103
104 await model.remove("NPM_TOKEN")
105
106 #expect(try body(try #require(stub.seen.first))["argv"] as? [String]
107 == ["repo", "secret", "remove", "krz/gitbay", "NPM_TOKEN"])
108 }
109}
110
111@MainActor
112struct DeployKeysViewModelTests {
113
114 @Test func listsKeysWithModeAndDates() async throws {
115 let (client, stub) = try makeClient()
116 stub.enqueue(.init(status: 200, json: deployKeysJSON))
117 let model = DeployKeysViewModel(client: client, repoPath: "krz/gitbay")
118 await model.load()
119
120 let keys = try #require(model.state.value)
121 #expect(keys.map(\.fingerprint) == ["SHA256:abc", "SHA256:def"])
122 #expect(keys[0].canPush && !keys[1].canPush)
123 #expect(keys[0].lastUsedAt != nil && keys[0].expiresAt == nil)
124 #expect(keys[1].lastUsedAt == nil && keys[1].expiresAt != nil)
125 #expect(stub.seen.first?.url.query() == "argv=repo&argv=deploy-key&argv=list&argv=krz/gitbay")
126 }
127
128 @Test func addSendsTheKeyOnStdinWithModeAndLifetime() async throws {
129 let (client, stub) = try makeClient()
130 for _ in 0..<2 {
131 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"fingerprint":"SHA256:x","mode":"ro"},"exit_code":0}"#, match: "cmd"))
132 stub.enqueue(.init(status: 200, json: deployKeysJSON, match: "argv=list"))
133 }
134 let model = DeployKeysViewModel(client: client, repoPath: "krz/gitbay")
135
136 #expect(await model.add(publicKey: "ssh-ed25519 AAAA ci\n", canPush: false, ttl: nil))
137 #expect(await model.add(publicKey: "ssh-ed25519 AAAA ci", canPush: true, ttl: "30d"))
138
139 let writes = try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map(body)
140 #expect(writes.map { $0["argv"] as? [String] } == [
141 ["repo", "deploy-key", "add", "krz/gitbay"],
142 ["repo", "deploy-key", "add", "krz/gitbay", "--rw", "--ttl", "30d"],
143 ])
144 #expect(writes.allSatisfy { $0["stdin"] as? String == "ssh-ed25519 AAAA ci" })
145 }
146
147 @Test func removeNamesTheFingerprint() async throws {
148 let (client, stub) = try makeClient()
149 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
150 stub.enqueue(.init(status: 200, json: deployKeysJSON, match: "argv=list"))
151 stub.enqueue(.init(status: 200, json: deployKeysJSON, match: "argv=list"))
152 let model = DeployKeysViewModel(client: client, repoPath: "krz/gitbay")
153 await model.load()
154
155 await model.remove(try #require(model.state.value?.first))
156
157 let writes = try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map(body)
158 #expect(writes.first?["argv"] as? [String] == ["repo", "deploy-key", "remove", "krz/gitbay", "SHA256:abc"])
159 }
160}