fix(12): IN-02 guard /setup/complete against skipping the credential step

Without a prerequisite check, an operator could call POST /api/setup/complete
directly, setting setup_complete=true with no admin user or credential row,
leaving no recovery path without manual DB surgery.

Add an inner join check for an unclaimed user with an associated credential;
return 422 if absent. Update /complete tests to seed the prerequisite for
the success path and add an explicit 422 regression test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-06-15 16:20:55 -04:00
co-authored by Claude Sonnet 4.6
parent d9dfe72aab
commit 3babbfa20e
2 changed files with 34 additions and 2 deletions
+17
View File
@@ -471,14 +471,31 @@ describe('POST /api/setup/credential', () => {
describe('POST /api/setup/complete — 423 guard (SETUP-04 / Pitfall 8)', () => {
it('returns 200 on first call (fresh setup, wizard not yet locked)', async () => {
// IN-02: /complete now requires an unclaimed user + credential before locking.
const userId = await seedLocalUser('complete-200');
await seedCredential(userId);
const app = await getApp();
const res = await app.fetch(jsonRequest('POST', '/api/setup/complete'));
expect(res.status).toBe(200);
});
it('returns 422 when /complete is called with no credential configured (IN-02 guard)', async () => {
// No unclaimed user or credential — /complete must refuse to lock setup.
const app = await getApp();
const res = await app.fetch(jsonRequest('POST', '/api/setup/complete'));
expect(res.status).toBe(422);
const body = (await res.json()) as { error: string };
expect(body.error).toMatch(/credential/i);
});
// This test is the load-bearing RED test — the 423 must be verified.
// Second call must return 423 because setup_complete is set after first call.
it('returns 423 on second call — setup already complete, wizard locked (Pitfall 8)', async () => {
// Seed prerequisite so first /complete call succeeds (IN-02 guard).
const userId = await seedLocalUser('complete-423');
await seedCredential(userId);
const app = await getApp();
// First call — should succeed and set setup_complete