Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions Sources/Auth/Internal/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,26 @@ struct APIClient: Sendable {
Dependencies[clientID].configuration
}

var sessionManager: SessionManager {
Dependencies[clientID].sessionManager
}

var eventEmitter: AuthStateChangeEventEmitter {
Dependencies[clientID].eventEmitter
}

var http: any HTTPClientType {
Dependencies[clientID].http
}

/// Error codes that should clean up local session.
private let sessionCleanupErrorCodes: [ErrorCode] = [
.sessionNotFound,
.sessionExpired,
.refreshTokenNotFound,
.refreshTokenAlreadyUsed,
]

func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse {
var request = request
request.headers = HTTPFields(configuration.headers).merging(with: request.headers)
Expand All @@ -42,7 +58,7 @@ struct APIClient: Sendable {
let response = try await http.send(request)

guard 200..<300 ~= response.statusCode else {
throw handleError(response: response)
throw await handleError(response: response)
}

return response
Expand All @@ -62,7 +78,7 @@ struct APIClient: Sendable {
return try await execute(request)
}

func handleError(response: Helpers.HTTPResponse) -> AuthError {
func handleError(response: Helpers.HTTPResponse) async -> AuthError {
guard
let error = try? response.decoded(
as: _RawAPIErrorResponse.self,
Expand Down Expand Up @@ -98,7 +114,12 @@ struct APIClient: Sendable {
message: error._getErrorMessage(),
reasons: error.weakPassword?.reasons ?? []
)
} else if errorCode == .sessionNotFound {
} else if let errorCode, sessionCleanupErrorCodes.contains(errorCode) {
// The `session_id` inside the JWT does not correspond to a row in the
// `sessions` table. This usually means the user has signed out, has been
// deleted, or their session has somehow been terminated.
await sessionManager.remove()
eventEmitter.emit(.signedOut, session: nil)
return .sessionMissing
} else {
return .api(
Expand Down
99 changes: 96 additions & 3 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,82 @@ final class AuthClientTests: XCTestCase {
)
}

func testRemoveSessionAndSignoutIfSessionNotFoundErrorReturned() async throws {
let sut = makeSUT()

Mock(
url: clientURL.appendingPathComponent("user"),
statusCode: 403,
data: [
.get: Data(
"""
{
"error_code": "session_not_found",
"message": "Session not found"
}
""".utf8
)
]
)
.register()

Dependencies[sut.clientID].sessionStorage.store(.validSession)

try await assertAuthStateChanges(
sut: sut,
action: {
do {
_ = try await sut.user()
XCTFail("Expected failure")
} catch {
XCTAssertEqual(error as? AuthError, .sessionMissing)
}
},
expectedEvents: [.initialSession, .signedOut]
)

XCTAssertNil(Dependencies[sut.clientID].sessionStorage.get())
}

func testRemoveSessionAndSignoutIfRefreshTokenNotFoundErrorReturned() async throws {
let sut = makeSUT()

Mock(
url: clientURL.appendingPathComponent("token").appendingQueryItems([
URLQueryItem(name: "grant_type", value: "refresh_token")
]),
statusCode: 403,
data: [
.post: Data(
"""
{
"error_code": "refresh_token_not_found",
"message": "Invalid Refresh Token: Refresh Token Not Found"
}
""".utf8
)
]
)
.register()

Dependencies[sut.clientID].sessionStorage.store(.expiredSession)

try await assertAuthStateChanges(
sut: sut,
action: {
do {
_ = try await sut.session
XCTFail("Expected failure")
} catch {
XCTAssertEqual(error as? AuthError, .sessionMissing)
}
},
expectedEvents: [.signedOut]
)

XCTAssertNil(Dependencies[sut.clientID].sessionStorage.get())
}

private func makeSUT(flowType: AuthFlowType = .pkce) -> AuthClient {
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.protocolClasses = [MockingURLProtocol.self]
Expand Down Expand Up @@ -2198,6 +2274,7 @@ final class AuthClientTests: XCTestCase {
action: () async throws -> T,
expectedEvents: [AuthChangeEvent],
expectedSessions: [Session?]? = nil,
timeout: TimeInterval = 2,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
Expand All @@ -2211,14 +2288,30 @@ final class AuthClientTests: XCTestCase {

let result = try await action()

let authStateChanges = await eventsTask.value
let authStateChanges = try await withTimeout(interval: timeout) {
await eventsTask.value
}
let events = authStateChanges.map(\.event)
let sessions = authStateChanges.map(\.session)

expectNoDifference(events, expectedEvents, fileID: fileID, filePath: filePath, line: line, column: column)
expectNoDifference(
events,
expectedEvents,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)

if let expectedSessions = expectedSessions {
expectNoDifference(sessions, expectedSessions, fileID: fileID, filePath: filePath, line: line, column: column)
expectNoDifference(
sessions,
expectedSessions,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}

return result
Expand Down
Loading