Files
honeyDueKMP/iosApp/HoneyDueUITests/SuiteZZ_CleanupTests.swift
treyt 5c360a2796 Rearchitect UI test suite for complete, non-flaky coverage against live API
- Migrate Suite4-10, SmokeTests, NavigationCriticalPathTests to AuthenticatedTestCase
  with seeded admin account and real backend login
- Add 34 accessibility identifiers across 11 app views (task completion, profile,
  notifications, theme, join residence, manage users, forms)
- Create FeatureCoverageTests (14 tests) covering previously untested features:
  profile edit, theme selection, notification prefs, task completion, manage users,
  join residence, task templates
- Create MultiUserSharingTests (18 API tests) and MultiUserSharingUITests (8 XCUI
  tests) for full cross-user residence sharing lifecycle
- Add cleanup infrastructure: SuiteZZ_CleanupTests auto-wipes test data after runs,
  cleanup_test_data.sh script for manual reset via admin API
- Add share code API methods to TestAccountAPIClient (generateShareCode, joinWithCode,
  getShareCode, listResidenceUsers, removeUser)
- Fix app bugs found by tests:
  - ResidencesListView join callback now uses forceRefresh:true
  - APILayer invalidates task cache when residence count changes
  - AllTasksView auto-reloads tasks when residence list changes
- Fix test quality: keyboard focus waits, Save/Add button label matching,
  Documents tab label (Docs), remove API verification from UI tests
- DataLayerTests and PasswordResetTests now verify through UI, not API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 17:32:13 -05:00

146 lines
5.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}