import XCTest /// Email-verification gating tests against the real backend's `RequireVerified` /// middleware. /// /// The backend policy changed so that ALL app-data endpoints now require a /// VERIFIED user (not merely an authenticated one). Previously only the /// share-code generation routes carried the gate; now the broad set of /// app-data reads/writes (residences, tasks, contractors, documents, /// notifications, subscription, …) are all gated behind verification. /// /// The app's UI-test mode bypasses email verification, so the rule /// "an unverified user is blocked from app data" is not exercised by the UI /// suite. These tests close that gap by hitting the gated endpoints directly /// with a real Kratos session token and asserting the policy: /// /// - VERIFIED required (403 for an authenticated-but-unverified user) on the /// app-data routes — e.g. `GET /residences/`, `GET /tasks/`, /// `GET /contractors/`, `GET /documents/`. /// - AUTHENTICATED-ONLY (must still work unverified — NOT 403): /// `GET /auth/me/`. The sign-up allow-list keeps these reachable so a freshly /// registered, not-yet-verified user can complete onboarding. /// - PUBLIC / lookup GETs (reachable without verification): e.g. /// `GET /tasks/categories/`. /// /// All tests run entirely via the API — no app launch required. Because the /// `RequireVerified` middleware fires BEFORE the handler, a 403 is produced for /// an unverified caller regardless of whether any data exists — so the /// negative-path reads need no seeded residences/tasks. final class AuthGatingAPITests: XCTestCase { /// App-data endpoints now gated behind email verification. Each is a read /// (GET) so no body is needed; the gate runs before the handler, so an /// unverified caller is rejected with 403 even with no seeded data. private let verifiedGatedDataPaths = [ "/residences/", "/tasks/", "/contractors/", "/documents/", ] /// Kratos identity emails created during a test, deleted in tearDown. private var createdEmails: [String] = [] /// Residence ids created during a test (verified path), deleted in tearDown. private var createdResidences: [(token: String, id: Int)] = [] override func setUpWithError() throws { continueAfterFailure = false guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)") } } override func tearDownWithError() throws { // True teardown: clean residences first (need their owner's token), then // remove every Kratos identity we provisioned. Both are idempotent and // run even if a test failed mid-way. for res in createdResidences { _ = TestAccountAPIClient.deleteResidence(token: res.token, id: res.id) } createdResidences.removeAll() for email in createdEmails { _ = TestAccountAPIClient.deleteKratosIdentity(email: email) } createdEmails.removeAll() try super.tearDownWithError() } // MARK: - Test 01: unverified user is blocked from app data (broad policy) /// An authenticated-but-UNVERIFIED account must be rejected by the /// `RequireVerified` gate with a 403 on every app-data endpoint. func test01_unverifiedUserBlockedFromAppData() throws { let runId = UUID().uuidString.prefix(6) let email = "authgate_unverified_\(runId)@test.com" createdEmails.append(email) guard let session = TestAccountAPIClient.createUnverifiedAccount( username: "authgate_unverified_\(runId)", email: email, password: "TestPass123!" ) else { XCTFail("Could not create unverified account") return } // Each gated data endpoint must return 403 for the unverified caller. // The gate fires before the handler, so no seeded data is required. for path in verifiedGatedDataPaths { let result = TestAccountAPIClient.rawRequest( method: "GET", path: path, token: session.token ) XCTAssertEqual( result.statusCode, 403, "Unverified user should be blocked by RequireVerified on GET \(path) with 403, got \(result.statusCode): \(result.errorBody ?? "nil")" ) } } // MARK: - Test 02: unverified user can still reach sign-up endpoints /// The sign-up allow-list must keep working for an unverified user, so a /// freshly registered account can complete onboarding before verifying. /// `GET /auth/me/` (authenticated-only) and lookup GETs (public) must NOT be /// blocked by the verification gate. func test02_unverifiedUserCanReachSignupEndpoints() throws { let runId = UUID().uuidString.prefix(6) let email = "authgate_signup_\(runId)@test.com" createdEmails.append(email) guard let session = TestAccountAPIClient.createUnverifiedAccount( username: "authgate_signup_\(runId)", email: email, password: "TestPass123!" ) else { XCTFail("Could not create unverified account") return } // `GET /auth/me/` is authenticated-only — an unverified token must still // succeed (200), proving the sign-up allow-list bypasses the gate. let me = TestAccountAPIClient.rawRequest( method: "GET", path: "/auth/me/", token: session.token ) XCTAssertNotEqual( me.statusCode, 403, "GET /auth/me/ must NOT be verification-gated for an unverified user, got 403: \(me.errorBody ?? "nil")" ) XCTAssertEqual( me.statusCode, 200, "Unverified user should reach GET /auth/me/ (sign-up allow-list), got \(me.statusCode): \(me.errorBody ?? "nil")" ) // A lookup GET is public reference data — reachable without // verification (not 401/403) even for an unverified caller. let categories = TestAccountAPIClient.rawRequest( method: "GET", path: "/tasks/categories/", token: session.token ) XCTAssertNotEqual( categories.statusCode, 401, "Lookup GET /tasks/categories/ should be reachable, got 401: \(categories.errorBody ?? "nil")" ) XCTAssertNotEqual( categories.statusCode, 403, "Lookup GET /tasks/categories/ should not be verification-gated, got 403: \(categories.errorBody ?? "nil")" ) } // MARK: - Test 03: verified user is not blocked (positive control) /// A VERIFIED account must pass the gate — `GET /residences/` must NOT /// return 403 (it returns 200). This proves Test 01's 403s are the /// verification gate and not an unrelated failure. func test03_verifiedUserNotBlocked() throws { let runId = UUID().uuidString.prefix(6) let email = "authgate_verified_\(runId)@test.com" createdEmails.append(email) guard let session = TestAccountAPIClient.createVerifiedAccount( username: "authgate_verified_\(runId)", email: email, password: "TestPass123!" ) else { XCTFail("Could not create verified account") return } let result = TestAccountAPIClient.rawRequest( method: "GET", path: "/residences/", token: session.token ) XCTAssertNotEqual( result.statusCode, 403, "Verified user should NOT be blocked by RequireVerified on GET /residences/, but got 403: \(result.errorBody ?? "nil")" ) XCTAssertEqual( result.statusCode, 200, "Verified user should pass the gate and list residences (200), got \(result.statusCode): \(result.errorBody ?? "nil")" ) } }