diff --git a/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift b/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift index 839b727..aaad90e 100644 --- a/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift +++ b/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift @@ -34,6 +34,23 @@ class AuthenticatedUITestCase: BaseUITestCase { } } + /// When `true`, every test in the suite forces a logout → login cycle + /// in `setUp`, guaranteeing a freshly-issued auth token on each run. + /// + /// Default is `false`: tests reuse the existing logged-in session + /// from the previous test in the same suite — much faster (one login + /// per suite, not one per test) and resilient to suites where the + /// current screen has no logout affordance (`UITestHelpers.ensureLoggedOut` + /// times out → the test fails before its body runs). + /// + /// Override to `true` in suites that have observed transient + /// `Invalid token` 401s on POST/PATCH while reads continue to work. + /// The recipe was added after a 2026-05 incident where the API + /// container was rebuilt mid-suite and in-memory tokens went stale. + /// In normal CI runs against a stable API + freshly-erased simulator, + /// session reuse is the correct default. + var forceFreshLoginPerTest: Bool { false } + override func setUpWithError() throws { guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Backend not reachable at \(TestAccountAPIClient.baseURL)") @@ -41,27 +58,27 @@ class AuthenticatedUITestCase: BaseUITestCase { try super.setUpWithError() - // If already logged in (tab bar visible), skip the login flow let tabBar = app.tabBars.firstMatch - if tabBar.waitForExistence(timeout: defaultTimeout) { - // Already logged in — just set up API session if needed - if needsAPISession { - guard let apiSession = TestAccountManager.loginSeededAccount( - username: apiCredentials.username, - password: apiCredentials.password - ) else { - XCTFail("Could not login API account '\(apiCredentials.username)'") - return - } - session = apiSession - cleaner = TestDataCleaner(token: apiSession.token) - } - return - } + let alreadyLoggedIn = tabBar.waitForExistence(timeout: defaultTimeout) - // Not logged in — do the full login flow - UITestHelpers.ensureLoggedOut(app: app) - loginToMainApp() + // Force-fresh path: log out (if needed) and re-authenticate per + // test so every test starts with a freshly-issued JWT. Catches + // server-side token invalidation that would otherwise surface + // mid-suite as opaque 401s on the first mutation call. + if forceFreshLoginPerTest { + if alreadyLoggedIn { + UITestHelpers.ensureLoggedOut(app: app) + } else { + UITestHelpers.ensureLoggedOut(app: app) + } + loginToMainApp() + } else if !alreadyLoggedIn { + // Legacy session-reuse path: only log in when not already in. + UITestHelpers.ensureLoggedOut(app: app) + loginToMainApp() + } + // (When `forceFreshLoginPerTest == false` AND we're already + // logged in, fall through with the existing session.) if needsAPISession { guard let apiSession = TestAccountManager.loginSeededAccount(