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:, /// 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 } }