Skip to content
Merged
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
80 changes: 80 additions & 0 deletions Tests/AuthTests/SessionStateMachineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,86 @@ final class SessionStateMachineTests: XCTestCase {
expectNoDifference(returnedSession, session)
}

func testRefreshCancellation_shouldRestoreUsableState() async throws {
// Store a session that will trigger a refresh when validSession() is called.
let currentSession = Session.expiredSession
Dependencies[clientID].sessionStorage.store(currentSession)

await http.when(
{ $0.url.path.contains("/token") },
return: { _ in
// Block until the task is cancelled; Task.sleep throws CancellationError on cancellation.
try await Task.sleep(nanoseconds: NSEC_PER_SEC * 60)
return .stub(Session.validSession)
}
)

let sut = sut
let refreshTask = Task {
try await sut.validSession()
}

await Task.yield()

// Cancel the in-flight refresh by removing the session.
await sut.remove()

// The task should have failed (CancellationError).
do {
_ = try await refreshTask.value
XCTFail("Expected failure after cancellation")
} catch {
// Expected: any error (CancellationError) is acceptable.
}

// State must be cleanly unauthenticated – not stuck in .refreshing.
do {
_ = try await sut.validSession()
XCTFail("Expected sessionMissing error after cancellation")
} catch AuthError.sessionMissing {
// Expected: state is clean.
}

// Recovery: a new session can be stored and retrieved (auto-refresh can proceed).
await sut.update(Session.validSession)
let session = try await sut.validSession()
expectNoDifference(session, Session.validSession)
}

func testRefreshFailure_storageAndStateConsistentAndRecoverable() async throws {
// Store a session that will trigger a refresh when validSession() is called.
let currentSession = Session.expiredSession
Dependencies[clientID].sessionStorage.store(currentSession)

struct RefreshError: Error {}
await http.when(
{ $0.url.path.contains("/token") },
return: { _ in throw RefreshError() }
)

// validSession() triggers a refresh that fails.
do {
_ = try await sut.validSession()
XCTFail("Expected refresh error")
} catch is RefreshError {
// Expected: the refresh error is propagated.
}

// State is unauthenticated after failure – validSession() throws sessionMissing.
do {
_ = try await sut.validSession()
XCTFail("Expected sessionMissing after refresh failure")
} catch AuthError.sessionMissing {
// Expected: storage and state are consistent.
}

// Auto-refresh recovery: update with a new session allows validSession() to succeed.
let newSession = Session.validSession
await sut.update(newSession)
let session = try await sut.validSession()
expectNoDifference(session, newSession)
}

func testSession_shouldRefreshSession_whenCurrentSessionExpired() async throws {
let currentSession = Session.expiredSession
Dependencies[clientID].sessionStorage.store(currentSession)
Expand Down
Loading