Files
honeyDueKMP/iosApp/HoneyDueUITests/SuiteZZ_CleanupTests.swift
T
Trey T c52ce4d497 Re-architect iOS XCUITest suite: per-test isolation + domain organization
Migrate the XCUITest suite off the legacy shared-account model (and the
prior Django-style auth assumptions) to a parallel-safe, domain-organized
architecture, validated end-to-end against the live Kratos stack.

Isolation (parallel-safe by construction):
- Core/Fixtures/TestAccount.swift: each test mints its own pre-verified
  Kratos identity (uit_<domain>_<uuid>@test.honeydue.local), logs in, seeds
  under its own token, and deletes the identity in teardown (cascading all
  data + clearing Kratos). No shared testuser; parallel workers no longer race.
- AuthenticatedUITestCase rewritten to that model (member surface preserved);
  adds requiresResidence / seedAccountPreconditions to seed UI-gated data
  BEFORE login (a fresh account is empty at login).

Organization (255 tests preserved, none dropped):
- 21 domain suites under Auth/ Onboarding/ Residence/ Task/ Contractor/
  Document/ Sharing/ Navigation/ Smoke/ CrossCutting/ E2E/, consistent
  <Domain>UITests naming. Removes the Suite1..11 / AAA_ / ZZ_ / Tests/Rebuild
  naming chaos and the overlapping task/residence/auth suites.

Runner + test plans:
- run_ui_tests.sh: Smoke gate -> Seed -> Parallel(8 workers) -> Sweep. The
  parallel phase runs the whole target minus phase-managed suites via
  -skip-testing, so new suites auto-include (no hand-maintained list to drift).
  Drops the 2-worker cap and Suite6 isolation (isolation made them moot).
- HoneyDueUITests.xctestplan skips the 4 phase-managed suites; adds Smoke.xctestplan.

Kratos auth fixes folded in (login/verify/reset endpoints removed under Kratos):
real Mailpit verification codes replace the obsolete fixed "123456"; teardown
deletes Kratos identities; admin-panel login uses the correct seeded password.

Build green; isolation, parallelism, and the precondition/sharing migrations
validated against the live stack (0 leaked accounts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:26:50 -05:00

144 lines
6.3 KiB
Swift

import XCTest
/// Phase 3 Cleanup tests run sequentially after all parallel suites.
/// Clears test data via the admin API, then re-seeds the required accounts.
///
/// CLEANUP ORDER (XCTest runs methods alphabetically):
/// testCleanup01_clearAllTestData admin-panel login + POST /admin/settings/clear-all-data
/// testCleanup01b_deleteKratosIdentities delete seeded Kratos identities (clean slate)
/// testCleanup02_reSeedTestUser re-create testuser via Kratos
/// testCleanup03_reSeedAdmin re-create admin via Kratos
///
/// WHY WE DELETE KRATOS IDENTITIES:
/// `clear-all-data` is LOCAL-ONLY it wipes residences/tasks and non-superuser
/// `auth_user` rows in Postgres, but it does NOT touch Kratos. The Kratos
/// identities (testuser@honeydue.com, admin@honeydue.com) survive the wipe, and
/// the backend also caches validated Kratos sessions in Redis (kratos_sess:<hash>,
/// 24h TTL). Left alone, that leaves orphaned/stale auth state across runs:
/// - Re-seeding via createVerifiedAccount would hit a Kratos 409 (identity exists).
/// - Tokens minted before the wipe map to now-deleted local user rows stale-session
/// errors until the next GET /auth/me/ lazily re-provisions the local user.
/// Deleting the Kratos identities after the local wipe makes re-seed a TRUE reset:
/// fresh identities, no 409, no orphaned sessions.
final class SuiteZZ_CleanupTests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
}
// MARK: - Clear All Data
func testCleanup01_clearAllTestData() {
let baseURL = TestAccountAPIClient.baseURL
// 1. Login to the admin PANEL (SQL super-admin: admin@honeydue.com / password123).
// This is a different system from the Kratos APP identity that happens to
// share the admin@honeydue.com email see AuthenticatedUITestCase for the
// full distinction. Admin API uses a Bearer token.
let adminToken = adminLogin(baseURL: baseURL, password: "password123")
XCTAssertNotNil(adminToken, "Admin login failed — cannot clear test data")
guard let token = adminToken else { return }
// 2. Call clear-all-data (LOCAL-ONLY wipe see class header).
let clearResult = adminClearAllData(baseURL: baseURL, token: token)
XCTAssertTrue(clearResult, "Failed to clear all test data via admin API")
}
// MARK: - Delete Kratos Identities
/// Runs between the local wipe (01) and re-seed (02). `clear-all-data` is
/// local-only, so the seeded Kratos identities survive it. Delete them here so
/// re-seeding creates fresh identities with no Kratos 409 and no orphaned/stale
/// auth state (see class header). Best-effort: deleteKratosIdentity is idempotent
/// (true if deleted or already absent); we log but do not hard-fail on false.
func testCleanup01b_deleteKratosIdentities() {
let deletedTestUser = TestAccountAPIClient.deleteKratosIdentity(email: "testuser@honeydue.com")
if !deletedTestUser {
NSLog("[Cleanup] deleteKratosIdentity(testuser@honeydue.com) returned false — continuing (best-effort)")
}
let deletedAdmin = TestAccountAPIClient.deleteKratosIdentity(email: "admin@honeydue.com")
if !deletedAdmin {
NSLog("[Cleanup] deleteKratosIdentity(admin@honeydue.com) returned false — continuing (best-effort)")
}
}
// MARK: - Re-Seed Accounts
func testCleanup02_reSeedTestUser() {
let session = TestAccountAPIClient.createVerifiedAccount(
username: "testuser",
email: "testuser@honeydue.com",
password: "TestPass123!"
)
XCTAssertNotNil(session, "Failed to re-seed testuser account after cleanup")
}
func testCleanup03_reSeedAdmin() {
let session = TestAccountAPIClient.createVerifiedAccount(
username: "admin",
email: "admin@honeydue.com",
password: "Test1234"
)
XCTAssertNotNil(session, "Failed to re-seed admin account after cleanup")
}
// MARK: - Private Helpers
/// Admin API uses `Bearer` token (not `Token` prefix), so we use inline URLRequest.
/// The admin-panel super-admin is admin@honeydue.com / password123.
private func adminLogin(baseURL: String, password: String = "password123") -> String? {
guard let url = URL(string: "\(baseURL)/admin/auth/login") else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
let body: [String: Any] = [
"email": "admin@honeydue.com",
"password": password
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
let semaphore = DispatchSemaphore(value: 0)
var token: String?
URLSession.shared.dataTask(with: request) { data, response, _ in
defer { semaphore.signal() }
guard let data = data,
let status = (response as? HTTPURLResponse)?.statusCode,
(200...299).contains(status),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let t = json["token"] as? String else { return }
token = t
}.resume()
semaphore.wait()
return token
}
private func adminClearAllData(baseURL: String, token: String) -> Bool {
guard let url = URL(string: "\(baseURL)/admin/settings/clear-all-data") else { return false }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 30
let semaphore = DispatchSemaphore(value: 0)
var success = false
URLSession.shared.dataTask(with: request) { _, response, _ in
defer { semaphore.signal() }
if let status = (response as? HTTPURLResponse)?.statusCode {
success = (200...299).contains(status)
}
}.resume()
semaphore.wait()
return success
}
}