From c9fbb1bf6e29be20f63a3ac99f5b14c33171c36e Mon Sep 17 00:00:00 2001 From: Ethan Dickson <ethan@coder.com> Date: Fri, 13 Dec 2024 13:34:01 +1100 Subject: [PATCH 1/2] chore: add API errors to SDK --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Preview Content/PreviewClient.swift | 38 +++--- Coder Desktop/Coder Desktop/SDK/Client.swift | 110 +++++++++++++++--- Coder Desktop/Coder Desktop/SDK/User.swift | 14 ++- .../Coder Desktop/Views/LoginForm.swift | 43 ++++--- .../Coder DesktopTests/AgentsTests.swift | 2 +- .../Coder DesktopTests/LoginFormTests.swift | 34 ++---- Coder Desktop/Coder DesktopTests/Util.swift | 6 +- .../Coder DesktopTests/VPNMenuTests.swift | 10 +- .../Coder DesktopTests/VPNStateTests.swift | 10 +- 10 files changed, 171 insertions(+), 98 deletions(-) diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cb887ce..065b859 100644 --- a/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "42dc2e0a0e0417a7f4f62b3e875c9559038beef7d2265073dd4fc81f2e11ee13", + "originHash" : "aa8dd97dc6e28dedc4a5c45c435467a247486474bf3c1caf5e67085d52325132", "pins" : [ { "identity" : "alamofire", diff --git a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift b/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift index e21e8c6..592fa83 100644 --- a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift +++ b/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift @@ -3,22 +3,26 @@ import SwiftUI struct PreviewClient: Client { init(url _: URL, token _: String? = nil) {} - func user(_: String) async throws -> User { - try await Task.sleep(for: .seconds(1)) - return User( - id: UUID(), - username: "admin", - avatar_url: "", - name: "admin", - email: "admin@coder.com", - created_at: Date.now, - updated_at: Date.now, - last_seen_at: Date.now, - status: "active", - login_type: "none", - theme_preference: "dark", - organization_ids: [], - roles: [] - ) + func user(_: String) async throws(ClientError) -> User { + do { + try await Task.sleep(for: .seconds(1)) + return User( + id: UUID(), + username: "admin", + avatar_url: "", + name: "admin", + email: "admin@coder.com", + created_at: Date.now, + updated_at: Date.now, + last_seen_at: Date.now, + status: "active", + login_type: "none", + theme_preference: "dark", + organization_ids: [], + roles: [] + ) + } catch { + throw ClientError.badResponse + } } } diff --git a/Coder Desktop/Coder Desktop/SDK/Client.swift b/Coder Desktop/Coder Desktop/SDK/Client.swift index 0e32e91..6058351 100644 --- a/Coder Desktop/Coder Desktop/SDK/Client.swift +++ b/Coder Desktop/Coder Desktop/SDK/Client.swift @@ -3,7 +3,7 @@ import Foundation protocol Client { init(url: URL, token: String?) - func user(_ ident: String) async throws -> User + func user(_ ident: String) async throws(ClientError) -> User } struct CoderClient: Client { @@ -25,38 +25,122 @@ struct CoderClient: Client { func request<T: Encodable>( _ path: String, method: HTTPMethod, - body: T - ) async -> DataResponse<Data, AFError> { + body: T? = nil + ) async throws(ClientError) -> HTTPResponse { let url = self.url.appendingPathComponent(path) - let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""] - return await AF.request( + let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] } + let out = await AF.request( url, method: method, parameters: body, - encoder: JSONParameterEncoder.default, headers: headers ).serializingData().response + guard let response = out.response else { + throw ClientError.noResponse + } + switch out.result { + case .success(let data): + return HTTPResponse(resp: response, data: data, req: out.request) + case .failure: + throw ClientError.badResponse + } } func request( _ path: String, method: HTTPMethod - ) async -> DataResponse<Data, AFError> { + ) async throws(ClientError) -> HTTPResponse { let url = self.url.appendingPathComponent(path) - let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""] - return await AF.request( + let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] } + let out = await AF.request( url, method: method, headers: headers ).serializingData().response + guard let response = out.response else { + throw ClientError.noResponse + } + switch out.result { + case .success(let data): + return HTTPResponse(resp: response, data: data, req: out.request) + case .failure: + throw ClientError.badResponse + } } + + func responseAsError(_ resp: HTTPResponse) throws(ClientError) -> APIError { + do { + let body = try CoderClient.decoder.decode(Response.self, from: resp.data) + return APIError( + response: body, + statusCode: resp.resp.statusCode, + method: resp.req?.httpMethod, + url: resp.req?.url + ) + } catch { + throw ClientError.badResponse + } + } + + enum Headers { + static let sessionToken = "Coder-Session-Token" + } + +} + +struct HTTPResponse { + let resp: HTTPURLResponse + let data: Data + let req: URLRequest? +} + +struct APIError: Decodable { + let response: Response + let statusCode: Int + let method: String? + let url: URL? + + var description: String { + var components: [String] = [] + if let method = method, let url = url { + components.append("\(method) \(url.absoluteString)") + } + components.append("Unexpected status code \(statusCode):\n\(response.message)") + if let detail = response.detail { + components.append("\tError: \(detail)") + } + if let validations = response.validations, !validations.isEmpty { + let validationMessages = validations.map { "\t\($0.field): \($0.detail)" } + components.append(contentsOf: validationMessages) + } + return components.joined(separator: "\n") + } +} + +struct Response: Decodable { + let message: String + let detail: String? + let validations: [ValidationError]? +} + +struct ValidationError: Decodable { + let field: String + let detail: String } enum ClientError: Error { - case unexpectedStatusCode + case apiError(APIError) case badResponse -} + case noResponse -enum Headers { - static let sessionToken = "Coder-Session-Token" + var description: String { + switch self { + case .apiError(let error): + return error.description + case .badResponse: + return "Bad response" + case .noResponse: + return "No response" + } + } } diff --git a/Coder Desktop/Coder Desktop/SDK/User.swift b/Coder Desktop/Coder Desktop/SDK/User.swift index 4ca26eb..789c34f 100644 --- a/Coder Desktop/Coder Desktop/SDK/User.swift +++ b/Coder Desktop/Coder Desktop/SDK/User.swift @@ -1,15 +1,17 @@ import Foundation extension CoderClient { - func user(_ ident: String) async throws -> User { - let resp = await request("/api/v2/users/\(ident)", method: .get) - guard let response = resp.response, response.statusCode == 200 else { - throw ClientError.unexpectedStatusCode + func user(_ ident: String) async throws(ClientError) -> User { + let res = try await request("/api/v2/users/\(ident)", method: .get) + guard res.resp.statusCode == 200 else { + let error = try responseAsError(res) + throw ClientError.apiError(error) } - guard let data = resp.data else { + do { + return try CoderClient.decoder.decode(User.self, from: res.data) + } catch { throw ClientError.badResponse } - return try CoderClient.decoder.decode(User.self, from: data) } } diff --git a/Coder Desktop/Coder Desktop/Views/LoginForm.swift b/Coder Desktop/Coder Desktop/Views/LoginForm.swift index 2888d0b..c4fe153 100644 --- a/Coder Desktop/Coder Desktop/Views/LoginForm.swift +++ b/Coder Desktop/Coder Desktop/Views/LoginForm.swift @@ -37,19 +37,21 @@ struct LoginForm<C: Client, S: Session>: View { } .animation(.easeInOut, value: currentPage) .onAppear { - loginError = nil baseAccessURL = session.baseAccessURL?.absoluteString ?? baseAccessURL sessionToken = "" - }.padding(.top, 35) - VStack(alignment: .center) { - if let loginError { - Text("\(loginError.description)") - .font(.headline) - .foregroundColor(.red) - .multilineTextAlignment(.center) + }.padding(.vertical, 35) + .alert("Error", isPresented: Binding( + get: { loginError != nil }, + set: { isPresented in + if !isPresented { + loginError = nil + } + } + )) { + Button("OK", role: .cancel) {}.keyboardShortcut(.defaultAction) + } message: { + Text(loginError?.description ?? "") } - } - .frame(height: 35) }.padding() .frame(width: 450, height: 220) .disabled(loading) @@ -57,9 +59,7 @@ struct LoginForm<C: Client, S: Session>: View { } internal func submit() async { - loginError = nil guard sessionToken != "" else { - loginError = .invalidToken return } guard let url = URL(string: baseAccessURL), url.scheme == "https" else { @@ -69,11 +69,10 @@ struct LoginForm<C: Client, S: Session>: View { loading = true defer { loading = false} let client = C(url: url, token: sessionToken) - do { + do throws(ClientError) { _ = try await client.user("me") } catch { - loginError = .failedAuth - print("Set error") + loginError = .failedAuth(error) return } session.store(baseAccessURL: url, sessionToken: sessionToken) @@ -142,7 +141,9 @@ struct LoginForm<C: Client, S: Session>: View { } private func next() { - loginError = nil + guard baseAccessURL != "" else { + return + } guard let url = URL(string: baseAccessURL), url.scheme == "https" else { loginError = .invalidURL return @@ -155,7 +156,6 @@ struct LoginForm<C: Client, S: Session>: View { private func back() { withAnimation { - loginError = nil currentPage = .serverURL focusedField = .baseAccessURL } @@ -164,17 +164,14 @@ struct LoginForm<C: Client, S: Session>: View { enum LoginError { case invalidURL - case invalidToken - case failedAuth + case failedAuth(ClientError) var description: String { switch self { case .invalidURL: return "Invalid URL" - case .invalidToken: - return "Invalid Session Token" - case .failedAuth: - return "Could not authenticate with Coder deployment" + case .failedAuth(let err): + return "Could not authenticate with Coder deployment:\n\(err.description)" } } } diff --git a/Coder Desktop/Coder DesktopTests/AgentsTests.swift b/Coder Desktop/Coder DesktopTests/AgentsTests.swift index e6c679a..2563362 100644 --- a/Coder Desktop/Coder DesktopTests/AgentsTests.swift +++ b/Coder Desktop/Coder DesktopTests/AgentsTests.swift @@ -56,7 +56,7 @@ struct AgentsTests { vpn.state = .connected vpn.agents = createMockAgents(count: 7) - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in var toggle = try view.find(ViewType.Toggle.self) #expect(try toggle.labelView().text().string() == "Show All") diff --git a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift index 3f3b547..3bdbf6a 100644 --- a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift +++ b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift @@ -18,7 +18,7 @@ struct LoginTests { @Test @MainActor func testInitialView() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in #expect(throws: Never.self) { try view.find(text: "Coder Desktop") } #expect(throws: Never.self) { try view.find(text: "Server URL") } @@ -30,11 +30,11 @@ struct LoginTests { @Test @MainActor func testInvalidServerURL() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in - try view.find(ViewType.TextField.self).setInput("") + try view.find(ViewType.TextField.self).setInput("http://") try view.find(button: "Next").tap() - #expect(throws: Never.self) { try view.find(text: "Invalid URL") } + #expect(throws: Never.self) { try view.find(ViewType.Alert.self) } } } } @@ -42,7 +42,7 @@ struct LoginTests { @Test @MainActor func testValidServerURL() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("https://coder.example.com") try view.find(button: "Next").tap() @@ -57,7 +57,7 @@ struct LoginTests { @Test @MainActor func testBackButton() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("https://coder.example.com") try view.find(button: "Next").tap() @@ -69,33 +69,19 @@ struct LoginTests { } } - @Test - @MainActor - func testInvalidSessionToken() async throws { - try await ViewHosting.host(view) { _ in - try await sut.inspection.inspect { view in - try view.find(ViewType.TextField.self).setInput("https://coder.example.com") - try view.find(button: "Next").tap() - try view.find(ViewType.SecureField.self).setInput("") - try await view.actualView().submit() - #expect(throws: Never.self) { try view.find(text: "Invalid Session Token") } - } - } - } - @Test @MainActor func testFailedAuthentication() async throws { let login = LoginForm<MockErrorClient, MockSession>() - try await ViewHosting.host(login.environmentObject(session)) { _ in + try await ViewHosting.host(login.environmentObject(session)) { try await login.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("https://coder.example.com") try view.find(button: "Next").tap() #expect(throws: Never.self) { try view.find(text: "Session Token") } try view.find(ViewType.SecureField.self).setInput("valid-token") try await view.actualView().submit() - #expect(throws: Never.self) { try view.find(text: "Could not authenticate with Coder deployment") } + #expect(throws: Never.self) { try view.find(ViewType.Alert.self) } } } } @@ -103,12 +89,12 @@ struct LoginTests { @Test @MainActor func testSuccessfulLogin() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("https://coder.example.com") try view.find(button: "Next").tap() try view.find(ViewType.SecureField.self).setInput("valid-token") - try view.find(button: "Sign In").tap() + try await view.actualView().submit() #expect(session.hasSession) } diff --git a/Coder Desktop/Coder DesktopTests/Util.swift b/Coder Desktop/Coder DesktopTests/Util.swift index cab9a8f..eb378ee 100644 --- a/Coder Desktop/Coder DesktopTests/Util.swift +++ b/Coder Desktop/Coder DesktopTests/Util.swift @@ -47,7 +47,7 @@ class MockSession: Session { struct MockClient: Client { init(url _: URL, token _: String? = nil) {} - func user(_: String) async throws -> Coder_Desktop.User { + func user(_: String) async throws(ClientError) -> Coder_Desktop.User { User( id: UUID(), username: "admin", @@ -68,9 +68,9 @@ struct MockClient: Client { struct MockErrorClient: Client { init(url: URL, token: String?) {} - func user(_ ident: String) async throws -> Coder_Desktop.User { + func user(_ ident: String) async throws(ClientError) -> Coder_Desktop.User { throw ClientError.badResponse } } -extension Inspection: @retroactive InspectionEmissary { } +extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary { } diff --git a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift index f7d482c..fb975e0 100644 --- a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift +++ b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift @@ -22,7 +22,7 @@ struct VPNMenuTests { func testVPNLoggedOut() async throws { session.hasSession = false - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in let toggle = try view.find(ViewType.Toggle.self) #expect(toggle.isDisabled()) @@ -35,7 +35,7 @@ struct VPNMenuTests { @Test @MainActor func testStartStopCalled() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in var toggle = try view.find(ViewType.Toggle.self) #expect(try !toggle.isOn()) @@ -62,7 +62,7 @@ struct VPNMenuTests { func testVPNDisabledWhileConnecting() async throws { vpn.state = .disabled - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in var toggle = try view.find(ViewType.Toggle.self) #expect(try !toggle.isOn()) @@ -83,7 +83,7 @@ struct VPNMenuTests { func testVPNDisabledWhileDisconnecting() async throws { vpn.state = .disabled - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in var toggle = try view.find(ViewType.Toggle.self) #expect(try !toggle.isOn()) @@ -108,7 +108,7 @@ struct VPNMenuTests { @Test @MainActor func testOffWhenFailed() async throws { - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in let toggle = try view.find(ViewType.Toggle.self) #expect(try !toggle.isOn()) diff --git a/Coder Desktop/Coder DesktopTests/VPNStateTests.swift b/Coder Desktop/Coder DesktopTests/VPNStateTests.swift index 5a2c960..c5a167e 100644 --- a/Coder Desktop/Coder DesktopTests/VPNStateTests.swift +++ b/Coder Desktop/Coder DesktopTests/VPNStateTests.swift @@ -20,7 +20,7 @@ struct VPNStateTests { func testDisabledState() async throws { vpn.state = .disabled - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in #expect(throws: Never.self) { try view.find(text: "Enable CoderVPN to see agents") @@ -34,7 +34,7 @@ struct VPNStateTests { func testConnectingState() async throws { vpn.state = .connecting - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in let progressView = try view.find(ViewType.ProgressView.self) #expect(try progressView.labelView().text().string() == "Starting CoderVPN...") @@ -47,7 +47,7 @@ struct VPNStateTests { func testDisconnectingState() async throws { vpn.state = .disconnecting - try await ViewHosting.host(view) { _ in + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in let progressView = try view.find(ViewType.ProgressView.self) #expect(try progressView.labelView().text().string() == "Stopping CoderVPN...") @@ -60,7 +60,7 @@ struct VPNStateTests { func testFailedState() async throws { vpn.state = .failed(.exampleError) - try await ViewHosting.host(view.environmentObject(vpn)) { _ in + try await ViewHosting.host(view.environmentObject(vpn)) { try await sut.inspection.inspect { view in let text = try view.find(ViewType.Text.self) #expect(try text.string() == VPNServiceError.exampleError.description) @@ -73,7 +73,7 @@ struct VPNStateTests { func testDefaultState() async throws { vpn.state = .connected - try await ViewHosting.host(view.environmentObject(vpn)) { _ in + try await ViewHosting.host(view.environmentObject(vpn)) { try await sut.inspection.inspect { view in #expect(throws: (any Error).self) { _ = try view.find(ViewType.Text.self) From a4ca27e37c797443c2fc7f40fd63ea270c9d9911 Mon Sep 17 00:00:00 2001 From: Ethan Dickson <ethan@coder.com> Date: Mon, 16 Dec 2024 21:59:56 +1100 Subject: [PATCH 2/2] review --- .../Preview Content/PreviewClient.swift | 3 +- Coder Desktop/Coder Desktop/SDK/Client.swift | 37 ++++++++----------- Coder Desktop/Coder Desktop/SDK/User.swift | 5 +-- Coder Desktop/Coder DesktopTests/Util.swift | 2 +- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift b/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift index 592fa83..336df0c 100644 --- a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift +++ b/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift @@ -1,4 +1,5 @@ import SwiftUI +import Alamofire struct PreviewClient: Client { init(url _: URL, token _: String? = nil) {} @@ -22,7 +23,7 @@ struct PreviewClient: Client { roles: [] ) } catch { - throw ClientError.badResponse + throw ClientError.reqError(AFError.explicitlyCancelled) } } } diff --git a/Coder Desktop/Coder Desktop/SDK/Client.swift b/Coder Desktop/Coder Desktop/SDK/Client.swift index 6058351..8f5e2c0 100644 --- a/Coder Desktop/Coder Desktop/SDK/Client.swift +++ b/Coder Desktop/Coder Desktop/SDK/Client.swift @@ -35,14 +35,11 @@ struct CoderClient: Client { parameters: body, headers: headers ).serializingData().response - guard let response = out.response else { - throw ClientError.noResponse - } switch out.result { case .success(let data): - return HTTPResponse(resp: response, data: data, req: out.request) - case .failure: - throw ClientError.badResponse + return HTTPResponse(resp: out.response!, data: data, req: out.request) + case .failure(let error): + throw ClientError.reqError(error) } } @@ -57,28 +54,26 @@ struct CoderClient: Client { method: method, headers: headers ).serializingData().response - guard let response = out.response else { - throw ClientError.noResponse - } switch out.result { case .success(let data): - return HTTPResponse(resp: response, data: data, req: out.request) - case .failure: - throw ClientError.badResponse + return HTTPResponse(resp: out.response!, data: data, req: out.request) + case .failure(let error): + throw ClientError.reqError(error) } } - func responseAsError(_ resp: HTTPResponse) throws(ClientError) -> APIError { + func responseAsError(_ resp: HTTPResponse) -> ClientError { do { let body = try CoderClient.decoder.decode(Response.self, from: resp.data) - return APIError( + let out = APIError( response: body, statusCode: resp.resp.statusCode, method: resp.req?.httpMethod, url: resp.req?.url ) + return ClientError.apiError(out) } catch { - throw ClientError.badResponse + return ClientError.unexpectedResponse(resp.data[...1024]) } } @@ -130,17 +125,17 @@ struct ValidationError: Decodable { enum ClientError: Error { case apiError(APIError) - case badResponse - case noResponse + case reqError(AFError) + case unexpectedResponse(Data) var description: String { switch self { case .apiError(let error): return error.description - case .badResponse: - return "Bad response" - case .noResponse: - return "No response" + case .reqError(let error): + return error.localizedDescription + case .unexpectedResponse(let data): + return "Unexpected response: \(data)" } } } diff --git a/Coder Desktop/Coder Desktop/SDK/User.swift b/Coder Desktop/Coder Desktop/SDK/User.swift index 789c34f..f9f20fa 100644 --- a/Coder Desktop/Coder Desktop/SDK/User.swift +++ b/Coder Desktop/Coder Desktop/SDK/User.swift @@ -4,13 +4,12 @@ extension CoderClient { func user(_ ident: String) async throws(ClientError) -> User { let res = try await request("/api/v2/users/\(ident)", method: .get) guard res.resp.statusCode == 200 else { - let error = try responseAsError(res) - throw ClientError.apiError(error) + throw responseAsError(res) } do { return try CoderClient.decoder.decode(User.self, from: res.data) } catch { - throw ClientError.badResponse + throw ClientError.unexpectedResponse(res.data[...1024]) } } } diff --git a/Coder Desktop/Coder DesktopTests/Util.swift b/Coder Desktop/Coder DesktopTests/Util.swift index eb378ee..f902dce 100644 --- a/Coder Desktop/Coder DesktopTests/Util.swift +++ b/Coder Desktop/Coder DesktopTests/Util.swift @@ -69,7 +69,7 @@ struct MockClient: Client { struct MockErrorClient: Client { init(url: URL, token: String?) {} func user(_ ident: String) async throws(ClientError) -> Coder_Desktop.User { - throw ClientError.badResponse + throw ClientError.reqError(.explicitlyCancelled) } }