@@ -72,6 +72,86 @@ final class SessionStateMachineTests: XCTestCase {
7272 expectNoDifference ( returnedSession, session)
7373 }
7474
75+ func testRefreshCancellation_shouldRestoreUsableState( ) async throws {
76+ // Store a session that will trigger a refresh when validSession() is called.
77+ let currentSession = Session . expiredSession
78+ Dependencies [ clientID] . sessionStorage. store ( currentSession)
79+
80+ await http. when (
81+ { $0. url. path. contains ( " /token " ) } ,
82+ return: { _ in
83+ // Block until the task is cancelled; Task.sleep throws CancellationError on cancellation.
84+ try await Task . sleep ( nanoseconds: NSEC_PER_SEC * 60 )
85+ return . stub( Session . validSession)
86+ }
87+ )
88+
89+ let sut = sut
90+ let refreshTask = Task {
91+ try await sut. validSession ( )
92+ }
93+
94+ await Task . yield ( )
95+
96+ // Cancel the in-flight refresh by removing the session.
97+ await sut. remove ( )
98+
99+ // The task should have failed (CancellationError).
100+ do {
101+ _ = try await refreshTask. value
102+ XCTFail ( " Expected failure after cancellation " )
103+ } catch {
104+ // Expected: any error (CancellationError) is acceptable.
105+ }
106+
107+ // State must be cleanly unauthenticated – not stuck in .refreshing.
108+ do {
109+ _ = try await sut. validSession ( )
110+ XCTFail ( " Expected sessionMissing error after cancellation " )
111+ } catch AuthError . sessionMissing {
112+ // Expected: state is clean.
113+ }
114+
115+ // Recovery: a new session can be stored and retrieved (auto-refresh can proceed).
116+ await sut. update ( Session . validSession)
117+ let session = try await sut. validSession ( )
118+ expectNoDifference ( session, Session . validSession)
119+ }
120+
121+ func testRefreshFailure_storageAndStateConsistentAndRecoverable( ) async throws {
122+ // Store a session that will trigger a refresh when validSession() is called.
123+ let currentSession = Session . expiredSession
124+ Dependencies [ clientID] . sessionStorage. store ( currentSession)
125+
126+ struct RefreshError : Error { }
127+ await http. when (
128+ { $0. url. path. contains ( " /token " ) } ,
129+ return: { _ in throw RefreshError ( ) }
130+ )
131+
132+ // validSession() triggers a refresh that fails.
133+ do {
134+ _ = try await sut. validSession ( )
135+ XCTFail ( " Expected refresh error " )
136+ } catch is RefreshError {
137+ // Expected: the refresh error is propagated.
138+ }
139+
140+ // State is unauthenticated after failure – validSession() throws sessionMissing.
141+ do {
142+ _ = try await sut. validSession ( )
143+ XCTFail ( " Expected sessionMissing after refresh failure " )
144+ } catch AuthError . sessionMissing {
145+ // Expected: storage and state are consistent.
146+ }
147+
148+ // Auto-refresh recovery: update with a new session allows validSession() to succeed.
149+ let newSession = Session . validSession
150+ await sut. update ( newSession)
151+ let session = try await sut. validSession ( )
152+ expectNoDifference ( session, newSession)
153+ }
154+
75155 func testSession_shouldRefreshSession_whenCurrentSessionExpired( ) async throws {
76156 let currentSession = Session . expiredSession
77157 Dependencies [ clientID] . sessionStorage. store ( currentSession)
0 commit comments