Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev committed Aug 29, 2024
1 parent bcbd1ca commit 15feaa8
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 98 deletions.
29 changes: 7 additions & 22 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -669,34 +669,24 @@ public final class AuthClient: Sendable {
let params = extractParams(from: url)

if configuration.flowType == .implicit, !isImplicitGrantFlow(params: params) {
throw SupabaseAuthImplicitGrantRedirectError(
message: "Not a valid implicit grant flow url: \(url)"
)
throw AuthError.implicitGrantRedirect(message: "Not a valid implicit grant flow url: \(url)")
}

if configuration.flowType == .pkce, !isPKCEFlow(params: params) {
throw SupabaseAuthPKCEGrantCodeExchangeError(
message: "Not a valid PKCE flow url: \(url)",
error: nil,
code: nil
)
throw AuthError.pkceGrantCodeExchange(message: "Not a valid PKCE flow url: \(url)")
}

if isPKCEFlow(params: params) {
guard let code = params["code"] else {
throw SupabaseAuthPKCEGrantCodeExchangeError(
message: "No code detected.",
error: nil,
code: nil
)
throw AuthError.pkceGrantCodeExchange(message: "No code detected.")
}

let session = try await exchangeCodeForSession(authCode: code)
return session
}

if params["error"] != nil || params["error_description"] != nil || params["error_code"] != nil {
throw SupabaseAuthPKCEGrantCodeExchangeError(
throw AuthError.pkceGrantCodeExchange(
message: params["error_description"] ?? "Error in URL with unspecified error_description.",
error: params["error"] ?? "unspecified_error",
code: params["error_code"] ?? "unspecified_code"
Expand All @@ -709,7 +699,7 @@ public final class AuthClient: Sendable {
let refreshToken = params["refresh_token"],
let tokenType = params["token_type"]
else {
throw SupabaseAuthImplicitGrantRedirectError(message: "No session defined in URL")
throw AuthError.implicitGrantRedirect(message: "No session defined in URL")
}

let expiresAt = params["expires_at"].flatMap(TimeInterval.init)
Expand Down Expand Up @@ -810,14 +800,9 @@ public final class AuthClient: Sendable {
headers: [.init(name: "Authorization", value: "Bearer \(accessToken)")]
)
)
} catch let error as SupabaseAuthAPIError {
} catch let AuthError.api(_, _, _, response) where ![404, 403, 401].contains(response.statusCode) {
// ignore 404s since user might not exist anymore
// ignore 401s, and 403s since an invalid or expired JWT should sign out the current session.
let ignoredCodes = Set([404, 403, 401])

if !ignoredCodes.contains(error.status) {
throw error
}
}
}

Expand Down Expand Up @@ -1174,7 +1159,7 @@ public final class AuthClient: Sendable {
@discardableResult
public func refreshSession(refreshToken: String? = nil) async throws -> Session {
guard let refreshToken = refreshToken ?? currentSession?.refreshToken else {
throw SupabaseAuthSessionMissingError()
throw AuthError.sessionMissing
}

return try await sessionManager.refreshSession(refreshToken)
Expand Down
81 changes: 37 additions & 44 deletions Sources/Auth/AuthError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,50 +100,43 @@ extension ErrorCode {
public static let InvalidCredentials = ErrorCode("invalid_credentials")
}

/// Represents an error thrown by Auth, either by the client or the server.
public protocol SupabaseAuthError: LocalizedError {
var message: String { get }
var errorCode: ErrorCode { get }
}

public struct SupabaseAuthSessionMissingError: SupabaseAuthError {
public let errorCode: ErrorCode = .SessionNotFound

public let message: String = "Auth session missing."
}

/// Error thrown on certain methods when the password used is deemed weak.
/// Inspect the reasons to identify what password strength rules are inadequate.
public struct SupabaseAuthWeakPasswordError: SupabaseAuthError {
public let errorCode: ErrorCode = .WeakPassword

public let message: String
public let reasons: [String]
}

public struct SupabaseAuthAPIError: SupabaseAuthError {
public let message: String
public let errorCode: ErrorCode

/// The Data response for the underlysing request which caused this error to be thrown.
public let underlyngData: Data

/// The response for the underlysing request which caused this error to be thrown.
public let underlyingResponse: HTTPURLResponse

/// The HTTP status code for the response which caused this error to be thrown.
public var status: Int { underlyingResponse.statusCode }
}

public struct SupabaseAuthPKCEGrantCodeExchangeError: SupabaseAuthError {
public let errorCode: ErrorCode = .Unknown
public enum AuthError: LocalizedError {
case sessionMissing
case weakPassword(message: String, reasons: [String])
case api(
message: String,
errorCode: ErrorCode,
underlyingData: Data,
underlyingResponse: HTTPURLResponse
)
case pkceGrantCodeExchange(message: String, error: String? = nil, code: String? = nil)
case implicitGrantRedirect(message: String)

public var message: String {
switch self {
case .sessionMissing: "Auth session missing."
case let .weakPassword(message, _),
let .api(message, _, _, _),
let .pkceGrantCodeExchange(message, _, _),
let .implicitGrantRedirect(message):
message
}
}

public let message: String
public let error: String?
public let code: String?
}
public var errorCode: ErrorCode {
switch self {
case .sessionMissing: .SessionNotFound
case .weakPassword: .WeakPassword
case let .api(_, errorCode, _, _): errorCode
case .pkceGrantCodeExchange, .implicitGrantRedirect: .Unknown
}
}

public struct SupabaseAuthImplicitGrantRedirectError: SupabaseAuthError {
public let errorCode: ErrorCode = .Unknown
public var message: String
public var errorDescription: String? {
if errorCode == .Unknown {
message
} else {
"\(errorCode.rawValue): \(message)"
}
}
}
4 changes: 1 addition & 3 deletions Sources/Auth/AuthMFA.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,12 @@ public struct AuthMFA: Sendable {
nextLevel: nextLevel,
currentAuthenticationMethods: currentAuthenticationMethods
)
} catch is SupabaseAuthSessionMissingError {
} catch AuthError.sessionMissing {
return AuthMFAGetAuthenticatorAssuranceLevelResponse(
currentLevel: nil,
nextLevel: nil,
currentAuthenticationMethods: []
)
} catch {
throw error
}
}
}
16 changes: 8 additions & 8 deletions Sources/Auth/Internal/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ struct APIClient: Sendable {
return try await execute(request)
}

func handleError(response: HTTPResponse) -> any SupabaseAuthError {
func handleError(response: HTTPResponse) -> AuthError {
guard let error = try? response.decoded(
as: _RawAPIErrorResponse.self,
decoder: configuration.decoder
) else {
return SupabaseAuthAPIError(
return .api(
message: "Unexpected error",
errorCode: .UnexpectedFailure,
underlyngData: response.data,
underlyingData: response.data,
underlyingResponse: response.underlyingResponse
)
}
Expand All @@ -84,22 +84,22 @@ struct APIClient: Sendable {
}

if errorCode == nil, let weakPassword = error.weakPassword {
return SupabaseAuthWeakPasswordError(
return .weakPassword(
message: error._getErrorMessage(),
reasons: weakPassword.reasons ?? []
)
} else if errorCode == .WeakPassword {
return SupabaseAuthWeakPasswordError(
return .weakPassword(
message: error._getErrorMessage(),
reasons: error.weakPassword?.reasons ?? []
)
} else if errorCode == .SessionNotFound {
return SupabaseAuthSessionMissingError()
return .sessionMissing
} else {
return SupabaseAuthAPIError(
return .api(
message: error._getErrorMessage(),
errorCode: errorCode ?? .Unknown,
underlyngData: response.data,
underlyingData: response.data,
underlyingResponse: response.underlyingResponse
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Auth/Internal/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private actor LiveSessionManager {
func session() async throws -> Session {
try await trace(using: logger) {
guard let currentSession = try sessionStorage.get() else {
throw SupabaseAuthSessionMissingError()
throw AuthError.sessionMissing
}

if !currentSession.isExpired {
Expand Down
17 changes: 7 additions & 10 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@ final class AuthClientTests: XCTestCase {
} catch {
assertInlineSnapshot(of: error, as: .dump) {
"""
▿ SupabaseAuthSessionMissingError
▿ errorCode: ErrorCode
- rawValue: "session_not_found"
- message: "Auth session missing."
- AuthError.sessionMissing
"""
}
Expand All @@ -125,10 +122,10 @@ final class AuthClientTests: XCTestCase {

func testSignOutShouldRemoveSessionIfUserIsNotFound() async throws {
sut = makeSUT { _ in
throw SupabaseAuthAPIError(
throw AuthError.api(
message: "",
errorCode: .Unknown,
underlyngData: Data(),
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 404, httpVersion: nil, headerFields: nil)!
)
}
Expand Down Expand Up @@ -156,10 +153,10 @@ final class AuthClientTests: XCTestCase {

func testSignOutShouldRemoveSessionIfJWTIsInvalid() async throws {
sut = makeSUT { _ in
throw SupabaseAuthAPIError(
throw AuthError.api(
message: "",
errorCode: .InvalidCredentials,
underlyngData: Data(),
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 401, httpVersion: nil, headerFields: nil)!
)
}
Expand Down Expand Up @@ -187,10 +184,10 @@ final class AuthClientTests: XCTestCase {

func testSignOutShouldRemoveSessionIf403Returned() async throws {
sut = makeSUT { _ in
throw SupabaseAuthAPIError(
throw AuthError.api(
message: "",
errorCode: .InvalidCredentials,
underlyngData: Data(),
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 403, httpVersion: nil, headerFields: nil)!
)
}
Expand Down
38 changes: 38 additions & 0 deletions Tests/AuthTests/AuthErrorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// AuthErrorTests.swift
// Supabase
//
// Created by Guilherme Souza on 29/08/24.
//

@testable import Auth
import XCTest

final class AuthErrorTests: XCTestCase {
func testErrors() {
let sessionMissing = AuthError.sessionMissing
XCTAssertEqual(sessionMissing.errorCode, .SessionNotFound)
XCTAssertEqual(sessionMissing.message, "Auth session missing.")

let weakPassword = AuthError.weakPassword(message: "Weak password", reasons: [])
XCTAssertEqual(weakPassword.errorCode, .WeakPassword)
XCTAssertEqual(weakPassword.message, "Weak password")

let api = AuthError.api(
message: "API Error",
errorCode: .EmailConflictIdentityNotDeletable,
underlyingData: Data(),
underlyingResponse: HTTPURLResponse(url: URL(string: "http://localhost")!, statusCode: 400, httpVersion: nil, headerFields: nil)!
)
XCTAssertEqual(api.errorCode, .EmailConflictIdentityNotDeletable)
XCTAssertEqual(api.message, "API Error")

let pkceGrantCodeExchange = AuthError.pkceGrantCodeExchange(message: "PKCE failure", error: nil, code: nil)
XCTAssertEqual(pkceGrantCodeExchange.errorCode, .Unknown)
XCTAssertEqual(pkceGrantCodeExchange.message, "PKCE failure")

let implicitGrantRedirect = AuthError.implicitGrantRedirect(message: "Implicit grant failure")
XCTAssertEqual(implicitGrantRedirect.errorCode, .Unknown)
XCTAssertEqual(implicitGrantRedirect.message, "Implicit grant failure")
}
}
7 changes: 3 additions & 4 deletions Tests/AuthTests/RequestsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,9 @@ final class RequestsTests: XCTestCase {
} catch {
assertInlineSnapshot(of: error, as: .dump) {
"""
▿ SupabaseAuthImplicitGrantRedirectError
▿ errorCode: ErrorCode
- rawValue: "unknown"
- message: "No session defined in URL"
▿ AuthError
▿ implicitGrantRedirect: (1 element)
- message: "No session defined in URL"
"""
}
Expand Down
9 changes: 3 additions & 6 deletions Tests/AuthTests/SessionManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import ConcurrencyExtras
import CustomDump
import Helpers
import InlineSnapshotTesting
import TestHelpers
import XCTest
import XCTestDynamicOverlay
import InlineSnapshotTesting

final class SessionManagerTests: XCTestCase {
var http: HTTPClientMock!
Expand Down Expand Up @@ -46,14 +46,11 @@ final class SessionManagerTests: XCTestCase {
func testSession_shouldFailWithSessionNotFound() async {
do {
_ = try await sut.session()
XCTFail("Expected a \(SupabaseAuthSessionMissingError()) failure")
XCTFail("Expected a \(AuthError.sessionMissing) failure")
} catch {
assertInlineSnapshot(of: error, as: .dump) {
"""
▿ SupabaseAuthSessionMissingError
▿ errorCode: ErrorCode
- rawValue: "session_not_found"
- message: "Auth session missing."
- AuthError.sessionMissing
"""
}
Expand Down

0 comments on commit 15feaa8

Please sign in to comment.