@testable import ForgejoKit import Testing struct ErrorMappingTests { @Test func classifiesHTTPStatusCodes() { #expect(HTTPErrorCategory(statusCode: 401) == .authentication) #expect(HTTPErrorCategory(statusCode: 403) == .permissionDenied) #expect(HTTPErrorCategory(statusCode: 404) == .notFound) #expect(HTTPErrorCategory(statusCode: 500) == .server) #expect(HTTPErrorCategory(statusCode: 503) == .server) #expect(HTTPErrorCategory(statusCode: 400) == .other) } @Test func exposesServiceErrorStatusAndCategory() { let forbidden = ServiceError.httpError(statusCode: 403, message: "missing scope") #expect(forbidden.httpStatusCode == 403) #expect(forbidden.httpErrorCategory == .permissionDenied) let serverError = ServiceError.httpError(statusCode: 502) #expect(serverError.httpStatusCode == 502) #expect(serverError.httpErrorCategory == .server) #expect(ServiceError.invalidURL.httpStatusCode == nil) #expect(ServiceError.invalidURL.httpErrorCategory == nil) } @Test func mapsAuthenticationStatusCodes() { #expect(AuthenticationError.from(statusCode: 401) == .invalidCredentials) #expect(AuthenticationError.from(statusCode: 401, body: "OTP required") == .otpRequired) #expect(AuthenticationError.from(statusCode: 403) == .unknownError(statusCode: 403)) #expect(AuthenticationError.from(statusCode: 404) == .serverNotFound) #expect(AuthenticationError.from(statusCode: 500) == .unknownError(statusCode: 500)) #expect(AuthenticationError.from(statusCode: 418) == .unknownError(statusCode: 418)) } @Test func mapsSecurityKeyBlockedBasicAuth() { // Forgejo's exact 401 body when a passkey/security key is enrolled on the account. let body = "Basic authorization is not allowed while having security keys enrolled" #expect(AuthenticationError.from(statusCode: 401, body: body) == .basicAuthBlockedBySecurityKey) #expect(AuthenticationError.basicAuthBlockedBySecurityKey.httpStatusCode == 401) #expect(AuthenticationError.basicAuthBlockedBySecurityKey.httpErrorCategory == .authentication) } @Test func exposesAuthenticationStatusAndCategory() { #expect(AuthenticationError.invalidCredentials.httpStatusCode == 401) #expect(AuthenticationError.invalidCredentials.httpErrorCategory == .authentication) #expect(AuthenticationError.serverNotFound.httpStatusCode == 404) #expect(AuthenticationError.serverNotFound.httpErrorCategory == .notFound) #expect(AuthenticationError.unknownError(statusCode: 403).httpStatusCode == 403) #expect(AuthenticationError.unknownError(statusCode: 403).httpErrorCategory == .permissionDenied) #expect(AuthenticationError.unknownError(statusCode: 502).httpErrorCategory == .server) #expect(AuthenticationError.certificateError.httpStatusCode == nil) #expect(AuthenticationError.certificateError.httpErrorCategory == nil) } // MARK: - ServiceError descriptions @Test func serviceErrorDescriptions() { #expect(ServiceError.noActiveInstance.errorDescription == "No active Forgejo instance") #expect(ServiceError.invalidURL.errorDescription == "Invalid URL") #expect(ServiceError.invalidResponse.errorDescription == "Invalid response from server") #expect(ServiceError.httpError(statusCode: 404).errorDescription == "HTTP error: 404") #expect(ServiceError.httpError(statusCode: 500).errorDescription == "HTTP error: 500") #expect(ServiceError.decodingFailed(detail: "missing key").errorDescription == "Decoding failed: missing key") } @Test func httpErrorDescriptionUsesForgejoMessageBody() { let error = ServiceError.httpError( statusCode: 405, message: #"{"message":"This pull request has failing status checks"}"#, ) #expect(error.errorDescription == "HTTP 405: This pull request has failing status checks") } @Test func httpErrorDescriptionFallsBackToRawBody() { let error = ServiceError.httpError(statusCode: 500, message: "database unavailable") #expect(error.errorDescription == "HTTP 500: database unavailable") } @Test func httpErrorDescriptionIgnoresWhitespaceOnlyMessage() { let error = ServiceError.httpError(statusCode: 503, message: " \n ") #expect(error.errorDescription == "HTTP error: 503") } @Test func httpErrorDescriptionFallsBackWhenJSONMessageFieldIsEmpty() { let error = ServiceError.httpError( statusCode: 422, message: #"{"message":" "}"#, ) #expect(error.errorDescription == "HTTP error: 422") } @Test func httpErrorDescriptionReturnsRawJSONWhenMessageFieldMissing() { let raw = #"{"errors":["bad request"]}"# let error = ServiceError.httpError(statusCode: 400, message: raw) #expect(error.errorDescription == "HTTP 400: \(raw)") } // MARK: - Merge error collapsing @Test func collapseMergeErrorMaps405WithEmptyBodyToNotMergeable() { let collapsed = PullRequestService.collapseMergeError( .httpError(statusCode: 405, message: ""), ) #expect(collapsed == .notMergeable) } @Test func collapseMergeErrorMaps409WithEmptyBodyToMergeConflict() { let collapsed = PullRequestService.collapseMergeError( .httpError(statusCode: 409, message: " \n "), ) #expect(collapsed == .mergeConflict) } @Test func collapseMergeErrorPreserves405WithBody() { let original = ServiceError.httpError( statusCode: 405, message: #"{"message":"failing status checks"}"#, ) #expect(PullRequestService.collapseMergeError(original) == original) } @Test func collapseMergeErrorPreserves409WithBody() { let original = ServiceError.httpError(statusCode: 409, message: "conflict in README.md") #expect(PullRequestService.collapseMergeError(original) == original) } @Test func collapseMergeErrorPassesOtherStatusCodesThrough() { let original = ServiceError.httpError(statusCode: 500, message: "") #expect(PullRequestService.collapseMergeError(original) == original) } @Test func collapseMergeErrorPassesNonHTTPErrorsThrough() { #expect(PullRequestService.collapseMergeError(.invalidURL) == .invalidURL) #expect(PullRequestService.collapseMergeError(.noActiveInstance) == .noActiveInstance) } }