import XCTest /// Post-suite cleanup that runs after all other test suites. /// /// Alphabetically `SuiteZZ` sorts after all `Suite0`–`Suite10` and `Tests/` classes, /// so this runs last in the test plan. It calls the admin API to wipe all test /// data, leaving the database clean for the next run. /// /// If the admin panel account isn't set up, cleanup is skipped (not failed). final class SuiteZZ_CleanupTests: XCTestCase { /// Admin panel credentials (separate from regular user auth). /// Default: admin@honeydue.com / password123 (seeded via `./dev.sh seed-admin`) private static let adminEmail = "admin@honeydue.com" private static let adminPassword = "password123" override func setUpWithError() throws { try super.setUpWithError() continueAfterFailure = true // Don't abort if cleanup partially fails } func test01_cleanupAllTestData() throws { guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Backend not reachable — skipping cleanup") } // Login to admin panel guard let adminToken = loginToAdminPanel() else { throw XCTSkip("Could not login to admin panel — is the admin user seeded? Run: ./dev.sh seed-admin") } // Call clear-all-data let result = clearAllData(token: adminToken) XCTAssertTrue(result.success, "Clear-all-data should succeed: \(result.message)") if result.success { print("[Cleanup] Deleted \(result.usersDeleted) users, preserved \(result.preserved) superadmins") } } func test02_reseedBaselineData() throws { guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Backend not reachable — skipping re-seed") } // Re-create the testuser and admin accounts so the DB is ready // for the next test run without needing Suite00. let testUser = SeededTestData.TestUser.self if let session = TestAccountAPIClient.createVerifiedAccount( username: testUser.username, email: testUser.email, password: testUser.password ) { SeededTestData.testUserToken = session.token print("[Cleanup] Re-seeded testuser account") } let admin = SeededTestData.AdminUser.self if let session = TestAccountAPIClient.createVerifiedAccount( username: admin.username, email: admin.email, password: admin.password ) { SeededTestData.adminUserToken = session.token print("[Cleanup] Re-seeded admin account") } } // MARK: - Admin API Helpers private struct AdminLoginResponse: Decodable { let token: String } private struct ClearResult { let success: Bool let usersDeleted: Int let preserved: Int let message: String } private func loginToAdminPanel() -> String? { let url = URL(string: "\(TestAccountAPIClient.baseURL.replacingOccurrences(of: "/api", with: ""))/api/admin/auth/login")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: [ "email": Self.adminEmail, "password": Self.adminPassword ]) request.timeoutInterval = 10 var result: String? let semaphore = DispatchSemaphore(value: 0) URLSession.shared.dataTask(with: request) { data, response, _ in defer { semaphore.signal() } guard let data = data, let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode), let decoded = try? JSONDecoder().decode(AdminLoginResponse.self, from: data) else { return } result = decoded.token }.resume() semaphore.wait() return result } private func clearAllData(token: String) -> ClearResult { let url = URL(string: "\(TestAccountAPIClient.baseURL.replacingOccurrences(of: "/api", with: ""))/api/admin/settings/clear-all-data")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.timeoutInterval = 30 var clearResult = ClearResult(success: false, usersDeleted: 0, preserved: 0, message: "No response") let semaphore = DispatchSemaphore(value: 0) URLSession.shared.dataTask(with: request) { data, response, error in defer { semaphore.signal() } guard let data = data, let httpResponse = response as? HTTPURLResponse else { clearResult = ClearResult(success: false, usersDeleted: 0, preserved: 0, message: error?.localizedDescription ?? "No response") return } if (200...299).contains(httpResponse.statusCode), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { clearResult = ClearResult( success: true, usersDeleted: json["users_deleted"] as? Int ?? 0, preserved: json["preserved_users"] as? Int ?? 0, message: json["message"] as? String ?? "OK" ) } else { let body = String(data: data, encoding: .utf8) ?? "?" clearResult = ClearResult(success: false, usersDeleted: 0, preserved: 0, message: "HTTP \(httpResponse.statusCode): \(body)") } }.resume() semaphore.wait() return clearResult } }