c52ce4d497
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>
129 lines
4.9 KiB
Swift
129 lines
4.9 KiB
Swift
import XCTest
|
|
|
|
/// A throwaway, fully-isolated test account.
|
|
///
|
|
/// The unit of isolation that lets suites run in parallel without sharing
|
|
/// state: each test mints its own unique, pre-verified Kratos identity, drives
|
|
/// the app's login UI as that identity, seeds data under its own token, and
|
|
/// deletes the identity in teardown — which cascades all of its data and
|
|
/// clears the Kratos identity in one call.
|
|
///
|
|
/// Email format is collision-proof so parallel workers never overlap, and
|
|
/// carries a recognizable prefix so `SweepFixture` can find leaked accounts:
|
|
/// uit_<domain>_<uuid>@test.honeydue.local
|
|
struct TestAccount {
|
|
let username: String
|
|
let email: String
|
|
let password: String
|
|
let session: TestSession
|
|
|
|
var token: String { session.token }
|
|
|
|
// MARK: - Identity generation
|
|
|
|
/// Recognizable prefix for every generated account, so leaks are findable.
|
|
static let emailPrefix = "uit_"
|
|
/// Domain used for all generated test accounts (never a real mailbox).
|
|
static let emailDomain = "test.honeydue.local"
|
|
|
|
static func uniqueEmail(domain: String) -> String {
|
|
let slug = domain.lowercased().replacingOccurrences(of: " ", with: "-")
|
|
let unique = UUID().uuidString.prefix(12).lowercased()
|
|
return "\(emailPrefix)\(slug)_\(unique)@\(emailDomain)"
|
|
}
|
|
|
|
/// True if an email belongs to the generated test-account namespace.
|
|
static func isGenerated(_ email: String) -> Bool {
|
|
email.hasPrefix(emailPrefix) && email.hasSuffix("@\(emailDomain)")
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/// Create a pre-verified, ready-to-use account via the Kratos admin API.
|
|
/// The identity is verified up front so login lands straight on the main
|
|
/// tabs (no email-verification gate). Fails the test if creation fails.
|
|
@discardableResult
|
|
static func create(
|
|
domain: String,
|
|
verified: Bool = true,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) -> TestAccount {
|
|
let email = uniqueEmail(domain: domain)
|
|
let username = String(email.split(separator: "@").first ?? "uituser")
|
|
let password = "UitPass123!"
|
|
|
|
let session: TestSession?
|
|
if verified {
|
|
session = TestAccountAPIClient.createVerifiedAccount(
|
|
username: username, email: email, password: password
|
|
)
|
|
} else {
|
|
session = TestAccountAPIClient.createUnverifiedAccount(
|
|
username: username, email: email, password: password
|
|
)
|
|
}
|
|
|
|
guard let session else {
|
|
XCTFail("Failed to create isolated test account \(email)", file: file, line: line)
|
|
preconditionFailure("account creation failed — see XCTFail above")
|
|
}
|
|
return TestAccount(username: username, email: email, password: password, session: session)
|
|
}
|
|
|
|
/// Delete the Kratos identity (cascades all app data). Best-effort —
|
|
/// never fails a test, since teardown cleanup should not mask the result.
|
|
func delete() {
|
|
_ = TestAccountAPIClient.deleteKratosIdentity(email: email)
|
|
}
|
|
|
|
// MARK: - UI login
|
|
|
|
/// Drive the app's login screen as this account and wait for the main tabs.
|
|
/// Assumes the app is on (or can reach) the standalone login screen.
|
|
func login(
|
|
into app: XCUIApplication,
|
|
timeout: TimeInterval,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) {
|
|
UITestHelpers.ensureOnLoginScreen(app: app)
|
|
|
|
let login = LoginScreenObject(app: app)
|
|
login.waitForLoad(timeout: timeout)
|
|
login.enterUsername(email) // Kratos identifier is the email
|
|
login.enterPassword(password)
|
|
|
|
let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton]
|
|
loginButton.waitForExistenceOrFail(timeout: timeout, file: file, line: line)
|
|
loginButton.tap()
|
|
}
|
|
|
|
// MARK: - Seeding (under this account's own token)
|
|
|
|
@discardableResult
|
|
func seedResidence(name: String? = nil) -> TestResidence {
|
|
TestDataSeeder.createResidence(token: token, name: name)
|
|
}
|
|
|
|
@discardableResult
|
|
func seedResidenceWithAddress(name: String? = nil) -> TestResidence {
|
|
TestDataSeeder.createResidenceWithAddress(token: token, name: name)
|
|
}
|
|
|
|
@discardableResult
|
|
func seedTask(residenceId: Int, title: String? = nil, fields: [String: Any] = [:]) -> TestTask {
|
|
TestDataSeeder.createTask(token: token, residenceId: residenceId, title: title, fields: fields)
|
|
}
|
|
|
|
@discardableResult
|
|
func seedContractor(name: String? = nil, fields: [String: Any] = [:]) -> TestContractor {
|
|
TestDataSeeder.createContractor(token: token, name: name, fields: fields)
|
|
}
|
|
|
|
@discardableResult
|
|
func seedDocument(residenceId: Int, title: String? = nil, documentType: String = "general") -> TestDocument {
|
|
TestDataSeeder.createDocument(token: token, residenceId: residenceId, title: title, documentType: documentType)
|
|
}
|
|
}
|