fix(20): CR-01 WR-06 atomic last-admin guard + empty-body 400

Wrap the last-admin check and UPDATE in a db.transaction with a
SELECT...FOR UPDATE locking read so concurrent PATCH demotions
serialise and cannot both pass the guard, eliminating the TOCTOU
race (CR-01).

Add a .refine() to updateMemberSchema requiring at least one field,
returning 400 via noEchoHook instead of crashing Drizzle with an
empty SET clause (WR-06).

Add Test H asserting empty {} -> 400 { error: 'Invalid request' }.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-06-18 17:59:56 -04:00
co-authored by Claude Sonnet 4.6
parent 5c74ada48b
commit 72977334fc
2 changed files with 70 additions and 28 deletions
+15
View File
@@ -1226,6 +1226,21 @@ describe('PATCH /api/admin/members/:id', () => {
);
expect(res.status).toBe(404);
});
// Test H (WR-06): empty {} body must return 400, not crash Drizzle with a 503
it('Test H (WR-06 empty body): PATCH with {} returns 400 { error: "Invalid request" }', async () => {
const adminId = await seedUser('admin-patch-empty', true);
const memberId = await seedUser('member-patch-empty', false);
currentDevUserId = adminId;
const app = await getApp();
const res = await app.fetch(
jsonRequest('PATCH', `/api/admin/members/${memberId}`, {}),
);
expect(res.status).toBe(400);
const body = (await res.json()) as { error: string };
expect(body.error).toBe('Invalid request');
});
});
// ===========================================================================