From d11cc82fece98881399e5fc096e508c3e379eb94 Mon Sep 17 00:00:00 2001 From: Trey T Date: Fri, 5 Jun 2026 23:35:28 -0500 Subject: [PATCH] Perf: inject auth token at launch to skip the UI login (~26-50% faster) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured: ~half of every authenticated test was fixed setup, dominated by the UI login (typing email+password, keyboard/SecureField dance, ~8-12s). The test already creates the account via API and holds its real Kratos session token — so instead of typing credentials, pass the token as a launch arg and boot the app already authenticated. - App (UITestRuntime + iOSApp): reads --ui-test-session-token; after the --reset-state clear, calls DataManager.setAuthToken(token) and replicates the post-login init the UI login path runs (getCurrentUser + initializeLookups + getMyResidences + getTasks) so owner-gated/data-gated screens (residence detail delete + manage-users, pickers, lists) work on boot. Guarded by UITestRuntime.isEnabled — no effect on production. - AuthenticatedUITestCase: in fresh-account mode, create the account + seed its preconditions BEFORE launch, expose the token via additionalLaunchArguments, and drop the UI login. Legacy (usesFreshAccount=false) suites still UI-login. Measured per-test medians: Contractor 34s -> 25s; Task (uses lookups) ~34s -> 16s. TESTING.md updated. All affected suites pass; 0 leaked accounts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Framework/AuthenticatedUITestCase.swift | 30 +++++++++++++------ iosApp/HoneyDueUITests/TESTING.md | 7 ++++- iosApp/iosApp/Helpers/UITestRuntime.swift | 12 ++++++++ iosApp/iosApp/iOSApp.swift | 20 +++++++++++++ 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift b/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift index 44c730a..7a1d90b 100644 --- a/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift +++ b/iosApp/HoneyDueUITests/Framework/AuthenticatedUITestCase.swift @@ -23,6 +23,18 @@ class AuthenticatedUITestCase: BaseUITestCase { /// logout-via-profile path was the #1 source of flakes under load). override var relaunchBetweenTests: Bool { true } + /// In fresh-account mode the app boots already authenticated via the + /// account's real Kratos token (the app reads `--ui-test-session-token` in + /// UITestRuntime) — skipping the slow, flaky UI login (~8-12s/test). The + /// account is created before the launch (see setUpWithError), so its token + /// is available here when BaseUITestCase assembles the launch arguments. + override var additionalLaunchArguments: [String] { + if usesFreshAccount, let token = account?.token { + return ["--ui-test-session-token", token] + } + return [] + } + /// Credentials for the Kratos APP identity used to seed data over the API. /// /// ⚠️ TWO DIFFERENT "admin@honeydue.com" EXIST — do NOT "fix" Test1234 to password123: @@ -119,25 +131,25 @@ class AuthenticatedUITestCase: BaseUITestCase { throw XCTSkip("Backend not reachable at \(TestAccountAPIClient.baseURL)") } - try super.setUpWithError() - if usesFreshAccount { - // Per-test isolation: relaunchBetweenTests gives every test a fresh - // app launch that lands on the login screen (--reset-state), so we - // log in as a brand-new pre-verified account, seed under its token, - // and delete it in teardown. No UI logout between tests. + // Per-test isolation WITHOUT a UI login: create the account and seed + // its UI-gated data via API BEFORE the app launches, then boot the + // app already authenticated via the injected session token (see + // `additionalLaunchArguments`). relaunchBetweenTests gives every test + // a fresh launch, so each boots as its own account; the account is + // deleted in teardown (cascading all its data). let acct = TestAccount.create(domain: accountDomain) account = acct session = acct.session cleaner = TestDataCleaner(token: acct.token) - // Seed UI-gated baseline data BEFORE login so the app loads it on - // its post-login fetch (a fresh account is otherwise empty). seedAccountPreconditions(acct) - acct.login(into: app, timeout: loginTimeout) + try super.setUpWithError() // launches with --ui-test-session-token waitForMainApp() return } + try super.setUpWithError() + // Legacy path: log in as a SPECIFIC seeded account (testCredentials), // optionally opening a separate API session (apiCredentials). let tabBar = app.tabBars.firstMatch diff --git a/iosApp/HoneyDueUITests/TESTING.md b/iosApp/HoneyDueUITests/TESTING.md index 89a5c70..3d26128 100644 --- a/iosApp/HoneyDueUITests/TESTING.md +++ b/iosApp/HoneyDueUITests/TESTING.md @@ -34,7 +34,12 @@ which in `setUp`: 1. mints a **unique, pre-verified Kratos identity** — `uit__@test.honeydue.local` (see `Core/Fixtures/TestAccount.swift`), -2. logs the app in as that account, +2. boots the app **already authenticated** by passing the account's real Kratos + token as `--ui-test-session-token` (the app reads it in `UITestRuntime` and + calls `DataManager.setAuthToken`). This skips the slow, flaky UI login + (~8–12s/test) — the app lands straight on the main tabs. Logged-OUT suites + (login/registration/onboarding) still drive the real login UI, since that's + what they test, 3. exposes `session` / `cleaner` / `account` for seeding under its **own** token, and in `tearDown` calls `account.delete()`, which **cascades all of the diff --git a/iosApp/iosApp/Helpers/UITestRuntime.swift b/iosApp/iosApp/Helpers/UITestRuntime.swift index 00ed76a..3f9fb07 100644 --- a/iosApp/iosApp/Helpers/UITestRuntime.swift +++ b/iosApp/iosApp/Helpers/UITestRuntime.swift @@ -10,6 +10,7 @@ enum UITestRuntime { static let resetStateFlag = "--reset-state" static let mockAuthFlag = "--ui-test-mock-auth" static let completeOnboardingFlag = "--complete-onboarding" + static let sessionTokenFlag = "--ui-test-session-token" // i18n-ignore-end static var launchArguments: [String] { @@ -36,6 +37,17 @@ enum UITestRuntime { isEnabled && launchArguments.contains(completeOnboardingFlag) } + /// A real Kratos session token supplied by an authenticated UI test so the + /// app can boot already logged in (skipping the slow/flaky UI login). The + /// test obtains this token when it creates the account via API. + static var injectedSessionToken: String? { + guard isEnabled else { return nil } + let args = launchArguments + guard let i = args.firstIndex(of: sessionTokenFlag), i + 1 < args.count else { return nil } + let token = args[i + 1] + return token.isEmpty ? nil : token + } + static func configureForLaunch() { guard isEnabled else { return } diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 0bc6141..565f0c1 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -54,6 +54,26 @@ struct iOSApp: App { // Initialize TokenStorage once at app startup (legacy support) TokenStorage.shared.initialize(manager: TokenManager.Companion.shared.getInstance()) + // UI-test auth injection: boot already authenticated with a real Kratos + // token (supplied by the test that created the account via API), skipping + // the slow, flaky UI login. Set AFTER the reset above so it isn't cleared. + // Also kick off the lookup init the UI login path would normally trigger, + // so pickers (categories, priorities, …) are populated. + if UITestRuntime.isEnabled, let token = UITestRuntime.injectedSessionToken { + DataManager.shared.setAuthToken(token: token) + // Replicate the post-login init the UI login path runs (initializeLookups + // + prefetchAllData) so data-gated screens — e.g. the residence detail + // toolbar (delete / manage-users) — have their data on boot. + Task { + // currentUser must be set or owner-gated UI (residence delete, + // manage-users) stays hidden — the UI login path fetches it too. + _ = try? await APILayer.shared.getCurrentUser(forceRefresh: true) + _ = try? await APILayer.shared.initializeLookups() + _ = try? await APILayer.shared.getMyResidences(forceRefresh: true) + _ = try? await APILayer.shared.getTasks(forceRefresh: true) + } + } + if !UITestRuntime.isEnabled { // Initialize PostHog Analytics (must use Swift AnalyticsManager, not the Kotlin stub) AnalyticsManager.shared.configure()