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:
co-authored by
Claude Sonnet 4.6
parent
5c74ada48b
commit
72977334fc
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user