Compare commits

..

No commits in common. "main" and "0.8.0" have entirely different histories.
main ... 0.8.0

4 changed files with 68 additions and 82 deletions

View file

@ -17,7 +17,7 @@ Add ForgejoKit as a dependency in your `Package.swift`:
```swift
dependencies: [
.package(url: "https://codeberg.org/secana/ForgejoKit.git", from: "0.8.1"),
.package(url: "https://codeberg.org/secana/ForgejoKit.git", from: "0.8.0"),
],
targets: [
.target(

View file

@ -1,73 +0,0 @@
import Foundation
public enum AuthenticationError: LocalizedError, Sendable, Equatable {
case invalidURL
case invalidCredentials
case otpRequired
case basicAuthBlockedBySecurityKey
case serverNotFound
case invalidResponse
case certificateError
case unknownError(statusCode: Int)
static func from(statusCode: Int, body: String = "") -> AuthenticationError {
switch statusCode {
case 401 where body.localizedCaseInsensitiveContains("security key"):
.basicAuthBlockedBySecurityKey
case 401:
body.localizedCaseInsensitiveContains("OTP") ? .otpRequired : .invalidCredentials
case 404:
.serverNotFound
default:
.unknownError(statusCode: statusCode)
}
}
public var httpStatusCode: Int? {
switch self {
case .invalidCredentials, .otpRequired, .basicAuthBlockedBySecurityKey:
401
case .serverNotFound:
404
case let .unknownError(statusCode):
statusCode
case .invalidURL, .invalidResponse, .certificateError:
nil
}
}
public var httpErrorCategory: HTTPErrorCategory? {
guard let httpStatusCode else {
return nil
}
return HTTPErrorCategory(statusCode: httpStatusCode)
}
public var errorDescription: String? {
switch self {
case .invalidURL:
"Invalid server URL"
case .invalidCredentials:
"Invalid username or password"
case .otpRequired:
"Two-factor authentication code required"
case .basicAuthBlockedBySecurityKey:
"An enrolled security key blocks Basic auth. Create a personal access token and use 'Login with Token'."
case .serverNotFound:
"Server not found. Please check the URL."
case .invalidResponse:
"Invalid response from server"
case .certificateError:
"SSL certificate error. Try enabling 'Accept Self-Signed Certificates' if your server uses one."
case let .unknownError(statusCode):
switch HTTPErrorCategory(statusCode: statusCode) {
case .permissionDenied:
"Permission denied (Status: \(statusCode))"
case .server:
"Server error occurred (Status: \(statusCode))"
case .authentication, .notFound, .other:
"Unknown error occurred (Status: \(statusCode))"
}
}
}
}

View file

@ -460,6 +460,73 @@ public enum HTTPErrorCategory: Equatable, Sendable {
}
}
public enum AuthenticationError: LocalizedError, Sendable, Equatable {
case invalidURL
case invalidCredentials
case otpRequired
case serverNotFound
case invalidResponse
case certificateError
case unknownError(statusCode: Int)
static func from(statusCode: Int, body: String = "") -> AuthenticationError {
switch statusCode {
case 401:
body.localizedCaseInsensitiveContains("OTP") ? .otpRequired : .invalidCredentials
case 404:
.serverNotFound
default:
.unknownError(statusCode: statusCode)
}
}
public var httpStatusCode: Int? {
switch self {
case .invalidCredentials, .otpRequired:
401
case .serverNotFound:
404
case let .unknownError(statusCode):
statusCode
case .invalidURL, .invalidResponse, .certificateError:
nil
}
}
public var httpErrorCategory: HTTPErrorCategory? {
guard let httpStatusCode else {
return nil
}
return HTTPErrorCategory(statusCode: httpStatusCode)
}
public var errorDescription: String? {
switch self {
case .invalidURL:
"Invalid server URL"
case .invalidCredentials:
"Invalid username or password"
case .otpRequired:
"Two-factor authentication code required"
case .serverNotFound:
"Server not found. Please check the URL."
case .invalidResponse:
"Invalid response from server"
case .certificateError:
"SSL certificate error. Try enabling 'Accept Self-Signed Certificates' if your server uses one."
case let .unknownError(statusCode):
switch HTTPErrorCategory(statusCode: statusCode) {
case .permissionDenied:
"Permission denied (Status: \(statusCode))"
case .server:
"Server error occurred (Status: \(statusCode))"
case .authentication, .notFound, .other:
"Unknown error occurred (Status: \(statusCode))"
}
}
}
}
public enum ServiceError: LocalizedError, Sendable, Equatable {
case noActiveInstance
case invalidURL

View file

@ -33,14 +33,6 @@ struct ErrorMappingTests {
#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)