test(12-01): Wave-0 test scaffolds — setup.test.ts + user.test.ts (RED)
- Add apps/api/tests/routes/setup.test.ts with it.todo() scaffolds for: SETUP-01 (GET /api/setup/status), SETUP-02 (validate/vapid + validate/oidc), SETUP-01 (POST /api/setup/credential), SETUP-04 (POST /api/setup/complete × 2 → first 200, second 423), D-10 effective-config 423 guard. All 15 cases RED (it.todo) so Plan 02 implements against real failing tests. - Extend apps/api/tests/auth/user.test.ts with D-08 first-login-claims describe block (5 it.todo() cases): unclaimed user bind, is_admin preservation, setup_complete=false fallthrough, no unclaimed fallthrough, no email lookup (D-10) - Suite collects clean: 375 passed | 20 todo — no import errors
This commit is contained in:
@@ -399,3 +399,42 @@ describe('upsertUser', () => {
|
|||||||
expect(mockDb.select).toHaveBeenCalledTimes(1);
|
expect(mockDb.select).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Plan 12-01: D-08 first-login-claims scaffold (Wave-0 RED placeholders) ──
|
||||||
|
//
|
||||||
|
// These tests cover the first-login-claims flow that Plan 12-02 implements in
|
||||||
|
// upsertUser. When setup_complete='true', the first OIDC login from an unknown
|
||||||
|
// iss+sub should "claim" the single unclaimed local user row (oidcIss IS NULL AND
|
||||||
|
// claimed=false), binding oidcIss/oidcSub and setting claimed=true.
|
||||||
|
//
|
||||||
|
// Key constraints (D-08 / D-10):
|
||||||
|
// - NEVER look up by email — only oidcIss+oidcSub and claimed=false
|
||||||
|
// - Preserve is_admin on the claimed row (operator pre-set it in the wizard)
|
||||||
|
// - Only claim when setup_complete='true' in app_config
|
||||||
|
|
||||||
|
describe('upsertUser — D-08 first-login-claims (Wave-0 scaffold, Plan 12-02 implements)', () => {
|
||||||
|
it.todo(
|
||||||
|
'when setup_complete is true and an unclaimed local user exists (oidcIss IS NULL, claimed=false), ' +
|
||||||
|
'binds oidcIss+oidcSub+claimed=true and returns the updated row',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'when setup_complete is true and claimed user row is found, ' +
|
||||||
|
'preserves is_admin on the claimed user (admin flag not overwritten)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'when setup_complete is false (or unset), does NOT check for unclaimed rows — ' +
|
||||||
|
'falls through to normal insert path',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'when setup_complete is true but NO unclaimed local user exists, ' +
|
||||||
|
'falls through to normal insert path (new user row created)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'first-login-claims NEVER uses email as a lookup key — ' +
|
||||||
|
'identity is strictly oidcIss IS NULL AND claimed=false (D-10)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Setup wizard route tests — Wave-0 scaffold (Plan 12-01).
|
||||||
|
*
|
||||||
|
* Covers SETUP-01, SETUP-02, SETUP-03, SETUP-04 + the 423 guard (Pitfall 8) + D-10
|
||||||
|
* effective-config branch. All tests are it.todo() placeholders at this stage —
|
||||||
|
* they are intentionally RED so the happy-path implementation (Plan 02) can be
|
||||||
|
* built TDD-green against them.
|
||||||
|
*
|
||||||
|
* The 423-guard test (SETUP-04) MUST be RED before the happy path is implemented.
|
||||||
|
* This matches the Wave-0 requirement in 12-RESEARCH.md §Validation Architecture.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - Tests import `app` (not setupRouter directly — mirrors admin.test.ts Pitfall 9 pattern)
|
||||||
|
* - Real DB integration against familysync_test (vitest globalSetup provisions it)
|
||||||
|
* - credentialSync.js and broker mocks isolate CalDAV calls
|
||||||
|
*
|
||||||
|
* Route surfaces tested:
|
||||||
|
* GET /api/setup/status
|
||||||
|
* POST /api/setup/validate/vapid
|
||||||
|
* POST /api/setup/validate/oidc
|
||||||
|
* POST /api/setup/credential
|
||||||
|
* POST /api/setup/complete
|
||||||
|
*
|
||||||
|
* SETUP-04 / Pitfall 8 — 423 guard:
|
||||||
|
* POST /api/setup/complete twice → first 200, second 423
|
||||||
|
* POST /api/setup/* when effectively configured (member_credentials + VAPID env) → 423
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'vitest';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mocks (mirrors admin.test.ts conventions — must be registered before `app` import)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// CalDAV credential validation — mock to avoid live Fastmail calls
|
||||||
|
// vi.mock('../../src/broker/credentialSync.js', () => ({
|
||||||
|
// validateEncryptAndStoreCredential: vi.fn(),
|
||||||
|
// CredentialValidationError: class extends Error {},
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
// CalDAV client — mock to avoid live network calls
|
||||||
|
// vi.mock('../../src/broker/client.js', () => ({
|
||||||
|
// createFastmailClient: vi.fn(),
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
// Outbox worker — avoid side-effects during tests
|
||||||
|
// vi.mock('../../src/broker/outboxWorker.js', () => ({
|
||||||
|
// startOutboxWorker: vi.fn(),
|
||||||
|
// initOutboxTrigger: vi.fn(),
|
||||||
|
// scheduleOutboxDrain: vi.fn(),
|
||||||
|
// runOutboxDrain: vi.fn(),
|
||||||
|
// __resetDrainState: vi.fn(),
|
||||||
|
// stopOutboxTrigger: vi.fn(),
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
// Dev-auth bypass — simulate unauthenticated requests to pre-auth /api/setup routes
|
||||||
|
// (unlike other routes, setup/* is pre-auth so no user injection needed for most tests)
|
||||||
|
// vi.mock('../../src/auth/devBypass.js', () => ({
|
||||||
|
// devAuthBypass: () => async (_c: unknown, next: () => Promise<void>) => next(),
|
||||||
|
// }));
|
||||||
|
//
|
||||||
|
// OIDC auth middleware — skip for pre-auth surface
|
||||||
|
// vi.mock('@hono/oidc-auth', () => ({
|
||||||
|
// oidcAuthMiddleware: () => async (_c: unknown, next: () => Promise<void>) => next(),
|
||||||
|
// processOAuthCallback: () => async (c: { json: (v: unknown) => unknown }) => c.json({ ok: true }),
|
||||||
|
// getAuth: () => null,
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SETUP-01: GET /api/setup/status
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('GET /api/setup/status', () => {
|
||||||
|
it.todo('returns { setupComplete: false } when no setup has been run (fresh state)');
|
||||||
|
|
||||||
|
it.todo('returns { setupComplete: true } after POST /api/setup/complete succeeds');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SETUP-02: POST /api/setup/validate/vapid
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('POST /api/setup/validate/vapid', () => {
|
||||||
|
it.todo('returns 200 { ok: true } for a valid VAPID key pair from generate-secrets');
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'returns 400 { ok: false } for a truncated/invalid VAPID public key (structural validation)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo('returns 400 for a missing privateKey field');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SETUP-02: POST /api/setup/validate/oidc
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('POST /api/setup/validate/oidc', () => {
|
||||||
|
it.todo(
|
||||||
|
'returns 400 { ok: false } when the OIDC issuer is unreachable (mocked network failure)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo('returns 400 for a missing issuer field');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SETUP-01: POST /api/setup/credential — CalDAV PROPFIND validation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('POST /api/setup/credential', () => {
|
||||||
|
it.todo(
|
||||||
|
'returns 400 when PROPFIND fails (simulates bad Fastmail app password or network error)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'returns 200 and stores encrypted credential when PROPFIND succeeds (mocked fetchCalendars)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'returns 400 with no echoed password when credential validation fails (Pitfall 7 / no-echo)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SETUP-04: POST /api/setup/complete — 423 guard (Pitfall 8, must be RED before plan 02)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('POST /api/setup/complete — 423 guard (SETUP-04 / Pitfall 8)', () => {
|
||||||
|
it.todo('returns 200 on first call (fresh setup, wizard not yet locked)');
|
||||||
|
|
||||||
|
// This test is the load-bearing RED test — the 423 must be verified before
|
||||||
|
// the happy-path POST /api/setup/complete is implemented. (SETUP-04 requirement)
|
||||||
|
it.todo('returns 423 on second call — setup already complete, wizard locked (Pitfall 8)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// D-10 effective-config branch: any /api/setup/* → 423 when effectively configured
|
||||||
|
// (member_credentials row exists AND VAPID env vars present)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('/api/setup/* — 423 when effectively configured (D-10 effective-config branch)', () => {
|
||||||
|
it.todo(
|
||||||
|
'any /api/setup/* route returns 423 when member_credentials row exists AND VAPID_PRIVATE_KEY + VAPID_PUBLIC_KEY env are set',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'does NOT return 423 when member_credentials row exists but VAPID env is absent (not fully configured)',
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'does NOT return 423 when VAPID env is set but no member_credentials row exists (not fully configured)',
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user