c0032ab7e1
Android UI Tests / ui-tests (push) Has been cancelled
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>
191 lines
7.9 KiB
Swift
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")"
|
|
)
|
|
}
|
|
}
|