From 3ce6ec6556346e68ef04a2dc366d80bcffde418c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:09:24 +0000 Subject: [PATCH 1/2] Initial plan From 2b38fc5fac7acb9a171812b3faf91d0a67b0ab33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:28:52 +0000 Subject: [PATCH 2/2] test(auth): add tests for refresh cancellation and failure in SessionStateMachine Co-authored-by: grdsdev <5923044+grdsdev@users.noreply.github.com> Agent-Logs-Url: https://github.com/supabase/supabase-swift/sessions/96bb2c8d-09ab-479b-89de-3bd718602e12 --- Tests/AuthTests/SessionManagerTests.swift | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/Tests/AuthTests/SessionManagerTests.swift b/Tests/AuthTests/SessionManagerTests.swift index 0d73006a7..44ffda586 100644 --- a/Tests/AuthTests/SessionManagerTests.swift +++ b/Tests/AuthTests/SessionManagerTests.swift @@ -72,6 +72,86 @@ final class SessionManagerTests: 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)