Files
honeyDueKMP/iosApp/HoneyDueAPITests/AuthGatingAPITests.swift
T
Trey T c0032ab7e1
Android UI Tests / ui-tests (push) Has been cancelled
Tests: assert verified-by-default API gating
Update AuthGatingAPITests for the backend's new policy (all app-data routes
require a verified email):
- unverified user -> 403 on GET /residences/, /tasks/, /contractors/, /documents/
- unverified user can still reach the sign-up allow-list: GET /auth/me/, and
  public lookups (GET /tasks/categories/)
- verified user -> 200 (positive control)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:49:45 -05:00

191 lines
7.9 KiB
Swift

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")"
)
}
}