Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a91361296a | |||
| 48de6ec7c4 | |||
| f1b0a53520 | |||
| f32e9a6889 | |||
| a1b27b5501 | |||
| ae0ce3824f | |||
| f98c5ccaf7 | |||
| 61f23e47f1 | |||
| ab5f08e4c0 | |||
| 5accf73363 | |||
| 299a157561 |
+188
-1
File diff suppressed because one or more lines are too long
@@ -455,6 +455,22 @@ Verifies the API process does not crash on transient boot-time DB connection res
|
||||
| TC-API-19.7 | Normal sign-in still works end-to-end | Follow TC-WEB-SSO-3 (SSO sign-in) on UAT | Successful sign-in, staff list visible — no regression from resilience changes |
|
||||
| TC-API-19.8 | Public routes unaffected during auth retry | While auth is retrying (TC-API-19.2 setup), `GET /api/branding` | 200 with branding data — public routes bypass auth and serve normally |
|
||||
|
||||
### 4.20 Portal OOBE — Create Client from Auth (GRO-2359)
|
||||
|
||||
Verifies the `POST /api/portal/clients-from-auth` endpoint that creates a new `clients` row for a first-time SSO user (out-of-box-experience registration). This endpoint requires a valid Better Auth session but does NOT require a portal session; it is the pre-portal step in the new-user OOBE flow.
|
||||
|
||||
| TC | Test Case | Steps | Expected Result |
|
||||
|----|-----------|-------|-----------------|
|
||||
| TC-API-20.1 | Successful client creation | 1. Sign in via SSO to obtain a Better Auth session<br>2. `POST /api/portal/clients-from-auth` with `{ "name": "Test User" }` | 201 `{ "id": "<uuid>", "name": "Test User", "email": "<sso-email>" }` — new `clients` row created |
|
||||
| TC-API-20.2 | All optional fields accepted | `POST /api/portal/clients-from-auth` with `{ "name": "Test User", "phone": "555-1234", "address": "1 Main St", "notes": "VIP" }` (authenticated) | 201 with `id`, `name`, `email`; row in DB has all four fields |
|
||||
| TC-API-20.3 | Invalid body — missing name | `POST /api/portal/clients-from-auth` with `{}` (authenticated) | 400 (Zod validation failure); no row created |
|
||||
| TC-API-20.4 | Invalid body — empty name | `POST /api/portal/clients-from-auth` with `{ "name": "" }` (authenticated) | 400 — name must be at least 1 character |
|
||||
| TC-API-20.5 | No session — 401 | `POST /api/portal/clients-from-auth` with a valid body but **no** Better Auth session cookie | 401 `{ "error": "Unauthorized" }` |
|
||||
| TC-API-20.6 | Existing email — 409 | 1. Create a client row whose email matches the signed-in SSO user's email<br>2. `POST /api/portal/clients-from-auth` as that user | 409 `{ "error": "A customer record with this email already exists" }` — no duplicate row |
|
||||
| TC-API-20.7 | Auth not configured — 503 | Temporarily disable auth (e.g., point `OIDC_ISSUER` to an invalid host) so `getAuth()` throws<br>2. `POST /api/portal/clients-from-auth` | 503 `{ "error": "Authentication not configured" }` — graceful degradation |
|
||||
| TC-API-20.8 | Concurrent insert race — 409 | Simulate two near-simultaneous requests from the same SSO user (before any row exists) | At most one request returns 201; the other returns 409 — no duplicate row, no 500 |
|
||||
|
||||
|
||||
## Pass/Fail Criteria
|
||||
|
||||
**Pass:**
|
||||
@@ -476,3 +492,4 @@ Verifies the API process does not crash on transient boot-time DB connection res
|
||||
## Update Policy
|
||||
|
||||
Any PR that changes user-facing behaviour MUST update this file. Test cases must be added, modified, or removed to reflect the new behaviour. The PR description must reference which playbook section was updated (e.g., "Updated UAT_PLAYBOOK.md §4.4 — new appointment rescheduling flow").
|
||||
|
||||
|
||||
@@ -69,9 +69,14 @@ describe("auth init", () => {
|
||||
beforeEach(() => {
|
||||
dbSelectResult = [];
|
||||
vi.clearAllMocks();
|
||||
// Stub fetch so OIDC discovery requests resolve instantly during tests.
|
||||
// Without this, AbortSignal.timeout(5000) in auth.ts races with vitest's
|
||||
// 5000ms default test timeout and causes flaky failures.
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
*/
|
||||
import postgres from "postgres";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import { execSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import * as schema from "./schema.js";
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const MIGRATIONS_FOLDER = resolve(__dirname, "../migrations");
|
||||
|
||||
async function reset() {
|
||||
const url = process.env.DATABASE_URL;
|
||||
@@ -73,7 +72,7 @@ async function reset() {
|
||||
// across processes regardless of how many connections the work uses —
|
||||
// it does NOT require the work to share the lock's session.
|
||||
//
|
||||
// Therefore `max` must be ≥ 2: 1 reserved for the lock + ≥1 free for
|
||||
// Therefore `max` must be >= 2: 1 reserved for the lock + >=1 free for
|
||||
// the work. `max: 1` would let `reserve()` consume the only connection
|
||||
// and every query inside the callback would block forever waiting for
|
||||
// a connection that never frees (connection-starvation deadlock). We
|
||||
@@ -122,7 +121,15 @@ async function reset() {
|
||||
console.log("✓ All tables and enums dropped\n");
|
||||
|
||||
console.log("Running migrations...");
|
||||
await migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
||||
// GRO-2672: drizzle-orm's migrate() has a high-water-mark bug that skips
|
||||
// migrations with stale `when` timestamps (0001, 0003, 0010, 0011). Use
|
||||
// drizzle-kit instead -- it applies migrations by hash, matching the K8s
|
||||
// migrate Job behaviour exactly.
|
||||
execSync("pnpm exec drizzle-kit migrate", {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env },
|
||||
cwd: resolve(__dirname, ".."),
|
||||
});
|
||||
console.log("✓ Migrations applied\n");
|
||||
|
||||
console.log("Seeding database...");
|
||||
|
||||
@@ -69,9 +69,14 @@ describe("auth init", () => {
|
||||
beforeEach(() => {
|
||||
dbSelectResult = [];
|
||||
vi.clearAllMocks();
|
||||
// Stub fetch so OIDC discovery requests resolve instantly during tests.
|
||||
// Without this, AbortSignal.timeout(5000) in auth.ts races with vitest's
|
||||
// 5000ms default test timeout and causes flaky failures.
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
|
||||
+6
-1
@@ -329,5 +329,10 @@ export async function initAuth(): Promise<void> {
|
||||
});
|
||||
})();
|
||||
|
||||
await authInitPromise;
|
||||
try {
|
||||
await authInitPromise;
|
||||
} catch (err) {
|
||||
authInitPromise = null; // allow retry on next call
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user