Commit 61a16cd9a0

61a16cd9a08bffa171f4a1e87ec5561b3e678b07

parent: bbb2bd6769

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-30 06:24 UTC

Explain the expiring-token refusal for keys and emails

An expiring token cannot create credentials (krz/gitbay#257), so adding
an SSH key or email address through the 90-day token the sign-in screen
suggests is always refused. Say what to do instead of showing the
command-shaped server message.

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
392struct 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}