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>
This commit is contained in:
@@ -3,30 +3,41 @@ import XCTest
|
|||||||
/// Email-verification gating tests against the real backend's `RequireVerified`
|
/// Email-verification gating tests against the real backend's `RequireVerified`
|
||||||
/// middleware.
|
/// 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
|
/// The app's UI-test mode bypasses email verification, so the rule
|
||||||
/// "an unverified user is blocked from the app" is not exercised by the UI
|
/// "an unverified user is blocked from app data" is not exercised by the UI
|
||||||
/// suite. These tests close that gap by hitting a `RequireVerified`-gated
|
/// suite. These tests close that gap by hitting the gated endpoints directly
|
||||||
/// endpoint directly with a real Kratos session token and asserting the gate
|
/// with a real Kratos session token and asserting the policy:
|
||||||
/// behaves as designed:
|
|
||||||
///
|
///
|
||||||
/// - Test A (negative): an UNVERIFIED account is rejected with **403** by the
|
/// - VERIFIED required (403 for an authenticated-but-unverified user) on the
|
||||||
/// verification gate.
|
/// app-data routes — e.g. `GET /residences/`, `GET /tasks/`,
|
||||||
/// - Test B (positive control): a VERIFIED account is NOT blocked (200/list),
|
/// `GET /contractors/`, `GET /documents/`.
|
||||||
/// proving the 403 in Test A is the verification gate and not an unrelated
|
/// - AUTHENTICATED-ONLY (must still work unverified — NOT 403):
|
||||||
/// failure.
|
/// `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/`.
|
||||||
///
|
///
|
||||||
/// Both run entirely via the API — no app launch required.
|
/// 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 {
|
final class AuthGatingAPITests: XCTestCase {
|
||||||
|
|
||||||
/// `POST /residences/:id/generate-share-code/` is wrapped with the
|
/// App-data endpoints now gated behind email verification. Each is a read
|
||||||
/// `RequireVerified` middleware in the Go router (see
|
/// (GET) so no body is needed; the gate runs before the handler, so an
|
||||||
/// `setupResidenceRoutes` — only the share-code/share-package generation
|
/// unverified caller is rejected with 403 even with no seeded data.
|
||||||
/// routes carry the gate). Because the middleware runs BEFORE the handler,
|
private let verifiedGatedDataPaths = [
|
||||||
/// an unverified caller is rejected with 403 regardless of whether the
|
"/residences/",
|
||||||
/// residence id exists — making it a clean, setup-free probe of the gate.
|
"/tasks/",
|
||||||
private func gatedPath(residenceId: Int) -> String {
|
"/contractors/",
|
||||||
"/residences/\(residenceId)/generate-share-code/"
|
"/documents/",
|
||||||
}
|
]
|
||||||
|
|
||||||
/// Kratos identity emails created during a test, deleted in tearDown.
|
/// Kratos identity emails created during a test, deleted in tearDown.
|
||||||
private var createdEmails: [String] = []
|
private var createdEmails: [String] = []
|
||||||
@@ -57,11 +68,11 @@ final class AuthGatingAPITests: XCTestCase {
|
|||||||
try super.tearDownWithError()
|
try super.tearDownWithError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Test A: unverified user is blocked
|
// MARK: - Test 01: unverified user is blocked from app data (broad policy)
|
||||||
|
|
||||||
/// An unverified account must be rejected by the `RequireVerified` gate with
|
/// An authenticated-but-UNVERIFIED account must be rejected by the
|
||||||
/// a 403 when it tries to reach a gated endpoint.
|
/// `RequireVerified` gate with a 403 on every app-data endpoint.
|
||||||
func test01_unverifiedUserIsBlockedWith403() throws {
|
func test01_unverifiedUserBlockedFromAppData() throws {
|
||||||
let runId = UUID().uuidString.prefix(6)
|
let runId = UUID().uuidString.prefix(6)
|
||||||
let email = "authgate_unverified_\(runId)@test.com"
|
let email = "authgate_unverified_\(runId)@test.com"
|
||||||
createdEmails.append(email)
|
createdEmails.append(email)
|
||||||
@@ -75,29 +86,80 @@ final class AuthGatingAPITests: XCTestCase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hit the RequireVerified-gated endpoint with the unverified session
|
// Each gated data endpoint must return 403 for the unverified caller.
|
||||||
// token. The gate runs before the handler, so a non-existent residence
|
// The gate fires before the handler, so no seeded data is required.
|
||||||
// id (999_999) still produces the 403 from the verification middleware
|
for path in verifiedGatedDataPaths {
|
||||||
// rather than a 404 from the handler — no residence setup required.
|
let result = TestAccountAPIClient.rawRequest(
|
||||||
let result = TestAccountAPIClient.rawRequest(
|
method: "GET",
|
||||||
method: "POST",
|
path: path,
|
||||||
path: gatedPath(residenceId: 999_999),
|
token: session.token
|
||||||
body: [:],
|
)
|
||||||
|
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
|
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(
|
XCTAssertEqual(
|
||||||
result.statusCode, 403,
|
me.statusCode, 200,
|
||||||
"Unverified user should be blocked by the RequireVerified gate with 403, got \(result.statusCode): \(result.errorBody ?? "nil")"
|
"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 B: verified user is not blocked (positive control)
|
// MARK: - Test 03: verified user is not blocked (positive control)
|
||||||
|
|
||||||
/// A verified account must pass the `RequireVerified` gate — i.e. the gated
|
/// A VERIFIED account must pass the gate — `GET /residences/` must NOT
|
||||||
/// endpoint must NOT return 403. This proves Test A's 403 is the
|
/// return 403 (it returns 200). This proves Test 01's 403s are the
|
||||||
/// verification gate, not an unrelated rejection.
|
/// verification gate and not an unrelated failure.
|
||||||
func test02_verifiedUserIsNotBlocked() throws {
|
func test03_verifiedUserNotBlocked() throws {
|
||||||
let runId = UUID().uuidString.prefix(6)
|
let runId = UUID().uuidString.prefix(6)
|
||||||
let email = "authgate_verified_\(runId)@test.com"
|
let email = "authgate_verified_\(runId)@test.com"
|
||||||
createdEmails.append(email)
|
createdEmails.append(email)
|
||||||
@@ -111,33 +173,18 @@ final class AuthGatingAPITests: XCTestCase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The verified caller must own a real residence so the share-code
|
|
||||||
// handler (which runs AFTER it passes the gate) returns a clean 200
|
|
||||||
// rather than a 404. This is what makes it a positive control: it
|
|
||||||
// exercises the gate AND succeeds past it.
|
|
||||||
guard let residence = TestAccountAPIClient.createResidence(
|
|
||||||
token: session.token,
|
|
||||||
name: "AuthGate Verified \(runId)"
|
|
||||||
) else {
|
|
||||||
XCTFail("Verified user should be able to create a residence")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
createdResidences.append((token: session.token, id: residence.id))
|
|
||||||
|
|
||||||
let result = TestAccountAPIClient.rawRequest(
|
let result = TestAccountAPIClient.rawRequest(
|
||||||
method: "POST",
|
method: "GET",
|
||||||
path: gatedPath(residenceId: residence.id),
|
path: "/residences/",
|
||||||
body: [:],
|
|
||||||
token: session.token
|
token: session.token
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertNotEqual(
|
XCTAssertNotEqual(
|
||||||
result.statusCode, 403,
|
result.statusCode, 403,
|
||||||
"Verified user should NOT be blocked by the RequireVerified gate, but got 403: \(result.errorBody ?? "nil")"
|
"Verified user should NOT be blocked by RequireVerified on GET /residences/, but got 403: \(result.errorBody ?? "nil")"
|
||||||
)
|
)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
result.statusCode, 200,
|
result.statusCode, 200,
|
||||||
"Verified user should pass the gate and generate a share code (200), got \(result.statusCode): \(result.errorBody ?? "nil")"
|
"Verified user should pass the gate and list residences (200), got \(result.statusCode): \(result.errorBody ?? "nil")"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user