Commit 198f6fc441
198f6fc441ed3bd147bc424fc846ffb3f81b7c78
parent: bbb2bd6769
Unsigned
cmc <hello@cleberg.net> · 2026-09-30 06:24 UTC
Explain the expiring-token refusal for keys and emails (!154)
Since krz/gitbay 89dd0a3 an expiring credential cannot run credential-minting commands (keys add, email add, email verify). The sign-in hint mints a 90-day token, so those actions are always refused in the app. `GitbayError.userFacingMessage` now maps that denial to: "This needs a token without an expiry. Add it on the web under Settings, or over SSH." Other denials pass through unchanged. 2 unit tests (579 pass), Release build. testAccountFlows' assertion follows the new text; not re-run live (token revoked).
Layout: unified · split
gitbay/Networking/GitbayError.swift
+4
| @@ -32,6 +32,10 @@ nonisolated enum GitbayError: LocalizedError, Sendable { |
| 32 | /// them loses the explanation. |
32 | /// them loses the explanation. |
| 33 | var userFacingMessage: String { |
33 | var userFacingMessage: String { |
| 34 | switch self { |
34 | switch self { |
| |
35 | // Since krz/gitbay#257 an expiring token cannot create credentials |
| |
36 | // (SSH keys, email addresses), and the sign-in hint mints one. |
| |
37 | case .denied(let message) where message.contains("creates a credential"): |
| |
38 | "This needs a token without an expiry. Add it on the web under Settings, or over SSH." |
| 35 | case .denied(let message), .notFound(let message): |
39 | case .denied(let message), .notFound(let message): |
| 36 | message |
40 | message |
| 37 | case .unauthorized: |
41 | case .unauthorized: |
gitbayTests/GitbayClientTests.swift
+16
| @@ -388,3 +388,19 @@ struct ETagStoreTests { |
| 388 | #expect(store.count == 0) |
388 | #expect(store.count == 0) |
| 389 | } |
389 | } |
| 390 | } |
390 | } |
| |
391 | |
| |
392 | struct ExpiringCredentialRefusalTests { |
| |
393 | |
| |
394 | @Test func theRefusalSaysWhatToDoInstead() { |
| |
395 | let error = GitbayError.denied( |
| |
396 | "keys add creates a credential, and the one this request came with expires; use a token or key without an expiry") |
| |
397 | #expect(error.userFacingMessage.contains("token without an expiry")) |
| |
398 | #expect(error.userFacingMessage.contains("Settings")) |
| |
399 | #expect(!error.userFacingMessage.contains("keys add")) |
| |
400 | } |
| |
401 | |
| |
402 | @Test func otherDenialsPassThrough() { |
| |
403 | #expect(GitbayError.denied("you are not a maintainer of krz/gitbay").userFacingMessage |
| |
404 | == "you are not a maintainer of krz/gitbay") |
| |
405 | } |
| |
406 | } |
gitbayUITests/LiveSmokeUITests.swift
+3 −3
| @@ -1006,10 +1006,10 @@ extension LiveSmokeUITests { |
| 1006 | app.descendants(matching: .any).matching(identifier: "key-paste-submit") |
1006 | app.descendants(matching: .any).matching(identifier: "key-paste-submit") |
| 1007 | .firstMatch.tap() |
1007 | .firstMatch.tap() |
| 1008 | // An expiring token (the suite's are) is refused before the key is |
1008 | // An expiring token (the suite's are) is refused before the key is |
| 1009 | // parsed; either refusal is the server's text reaching the sheet. |
1009 | // parsed; either refusal reaching the sheet passes. |
| 1010 | XCTAssertTrue(app.staticTexts |
1010 | XCTAssertTrue(app.staticTexts |
| 1011 | .containing(NSPredicate(format: |
1011 | .containing(NSPredicate(format: |
| 1012 | "label CONTAINS 'not a valid public key' OR label CONTAINS 'creates a credential'")) |
1012 | "label CONTAINS 'not a valid public key' OR label CONTAINS 'token without an expiry'")) |
| 1013 | .firstMatch.waitForExistence(timeout: 15), "invalid-key refusal not surfaced") |
1013 | .firstMatch.waitForExistence(timeout: 15), "invalid-key refusal not surfaced") |
| 1014 | app.buttons["Cancel"].firstMatch.tap() |
1014 | app.buttons["Cancel"].firstMatch.tap() |
| 1015 | |
1015 | |
| @@ -1069,7 +1069,7 @@ extension LiveSmokeUITests { |
| 1069 | verify.tap() |
1069 | verify.tap() |
| 1070 | XCTAssertTrue(app.staticTexts |
1070 | XCTAssertTrue(app.staticTexts |
| 1071 | .containing(NSPredicate(format: |
1071 | .containing(NSPredicate(format: |
| 1072 | "label CONTAINS 'invalid, expired' OR label CONTAINS 'creates a credential'")) |
1072 | "label CONTAINS 'invalid, expired' OR label CONTAINS 'token without an expiry'")) |
| 1073 | .firstMatch.waitForExistence(timeout: 15), "bad-code refusal not surfaced") |
1073 | .firstMatch.waitForExistence(timeout: 15), "bad-code refusal not surfaced") |
| 1074 | } |
1074 | } |
| 1075 | } |
1075 | } |