| @@ -39,11 +39,11 @@ private let adminDashboardJSON = """ |
| 39 | }},"exit_code":0} |
39 | }},"exit_code":0} |
| 40 | """ |
40 | """ |
| 41 | |
41 | |
| 42 | nonisolated private struct Envelope: Decodable { let data: AdminDashboard } |
42 | nonisolated private struct DashboardEnvelope: Decodable { let data: AdminDashboard } |
| 43 | |
43 | |
| 44 | private func sections() throws -> [QueueSection] { |
44 | private func sections() throws -> [QueueSection] { |
| 45 | let dashboard = try GitbayClient.decoder() |
45 | let dashboard = try GitbayClient.decoder() |
| 46 | .decode(Envelope.self, from: Data(adminDashboardJSON.utf8)).data |
46 | .decode(DashboardEnvelope.self, from: Data(adminDashboardJSON.utf8)).data |
| 47 | let queues = try #require(dashboard.queues) |
47 | let queues = try #require(dashboard.queues) |
| 48 | // A fixed stand-in for relative time keeps the text deterministic. |
48 | // A fixed stand-in for relative time keeps the text deterministic. |
| 49 | return queues.sections(when: { "at \($0)" }) |
49 | return queues.sections(when: { "at \($0)" }) |
| @@ -94,7 +94,7 @@ struct AdminQueueSectionTests { |
| 94 | /// Everyone else's dashboard omits both blocks. |
94 | /// Everyone else's dashboard omits both blocks. |
| 95 | @Test func aNonAdminDashboardHasNoQueues() throws { |
95 | @Test func aNonAdminDashboardHasNoQueues() throws { |
| 96 | let json = #"{"protocol_version":1,"data":{"unread":3},"exit_code":0}"# |
96 | let json = #"{"protocol_version":1,"data":{"unread":3},"exit_code":0}"# |
| 97 | let dashboard = try GitbayClient.decoder().decode(Envelope.self, from: Data(json.utf8)).data |
97 | let dashboard = try GitbayClient.decoder().decode(DashboardEnvelope.self, from: Data(json.utf8)).data |
| 98 | #expect(dashboard.queues == nil) |
98 | #expect(dashboard.queues == nil) |
| 99 | #expect(dashboard.server == nil) |
99 | #expect(dashboard.server == nil) |
| 100 | } |
100 | } |
| @@ -243,6 +243,18 @@ struct AdminUsersViewModelTests { |
| 243 | #expect(stub.seen.first?.url.query() == "argv=admin&argv=user&argv=list&argv=--limit&argv=50") |
243 | #expect(stub.seen.first?.url.query() == "argv=admin&argv=user&argv=list&argv=--limit&argv=50") |
| 244 | } |
244 | } |
| 245 | |
245 | |
| |
246 | @Test func loadIfNeededOnlyReadsOnce() async throws { |
| |
247 | let (client, stub) = try makeClient() |
| |
248 | stub.enqueue(.init(status: 200, json: userPageJSON)) |
| |
249 | let model = AdminUsersViewModel(client: client) |
| |
250 | |
| |
251 | await model.loadIfNeeded() |
| |
252 | await model.loadIfNeeded() |
| |
253 | |
| |
254 | #expect(stub.seen.count == 1) |
| |
255 | #expect(stub.seen.first?.url.query() == "argv=admin&argv=user&argv=list&argv=--limit&argv=50") |
| |
256 | } |
| |
257 | |
| 246 | @Test func aFilterReloadsWithItsState() async throws { |
258 | @Test func aFilterReloadsWithItsState() async throws { |
| 247 | let (client, stub) = try makeClient() |
259 | let (client, stub) = try makeClient() |
| 248 | stub.enqueue(.init(status: 200, json: userPageJSON)) |
260 | stub.enqueue(.init(status: 200, json: userPageJSON)) |
| @@ -277,14 +289,36 @@ struct AdminUsersViewModelTests { |
| 277 | #expect(model.list.state.value?.count == 2) |
289 | #expect(model.list.state.value?.count == 2) |
| 278 | } |
290 | } |
| 279 | |
291 | |
| |
292 | /// The last-admin refusal is a real server failure (exit 1, HTTP 500), |
| |
293 | /// which the client retries once before surfacing it. |
| 280 | @Test func aRefusalIsShownVerbatim() async throws { |
294 | @Test func aRefusalIsShownVerbatim() async throws { |
| 281 | let (client, stub) = try makeClient() |
295 | let (client, stub) = try makeClient() |
| 282 | stub.enqueue(.init(status: 200, json: |
296 | let refusal = StubProtocol.Stub(status: 500, json: |
| 283 | #"{"protocol_version":1,"error":"cmc is the last admin","exit_code":4}"#, match: "cmd")) |
297 | #"{"protocol_version":1,"error":"that is the only instance admin; promote someone else first","exit_code":1}"#, |
| |
298 | match: "cmd") |
| |
299 | stub.enqueue(refusal) |
| |
300 | stub.enqueue(refusal) |
| |
301 | stub.enqueue(.init(status: 200, json: userPageJSON, match: "argv=list")) |
| 284 | let model = AdminUsersViewModel(client: client) |
302 | let model = AdminUsersViewModel(client: client) |
| 285 | |
303 | |
| 286 | await model.perform(.demote, on: "cmc") |
304 | await model.perform(.demote, on: "cmc") |
| 287 | |
305 | |
| 288 | #expect(model.actionError == "cmc is the last admin") |
306 | #expect(model.actionError == "that is the only instance admin; promote someone else first") |
| |
307 | #expect(model.list.state.value?.count == 2) |
| |
308 | } |
| |
309 | |
| |
310 | /// A refusal must still reload the list, so a stale row does not |
| |
311 | /// linger after the account changed state meanwhile. |
| |
312 | @Test func aRefusedActionStillReloadsTheList() async throws { |
| |
313 | let (client, stub) = try makeClient() |
| |
314 | stub.enqueue(.init(status: 403, json: |
| |
315 | #"{"protocol_version":1,"error":"account changed state","exit_code":4}"#, match: "cmd")) |
| |
316 | stub.enqueue(.init(status: 200, json: userPageJSON, match: "argv=list")) |
| |
317 | let model = AdminUsersViewModel(client: client) |
| |
318 | |
| |
319 | await model.perform(.disable, on: "bhargavkk") |
| |
320 | |
| |
321 | #expect(model.actionError == "account changed state") |
| |
322 | #expect(stub.seen.contains { $0.url.query()?.contains("argv=list") == true }) |
| 289 | } |
323 | } |
| 290 | } |
324 | } |