- 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>
146 lines
5.9 KiB
Swift
146 lines
5.9 KiB
Swift
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
|
||
}
|
||
}
|