test(invites): tighten check-invite assertions to byte-identical responses

Per quality review: replace per-property assertions with toEqual()
object-equality on the invalid-response bodies. Locks the enumeration-
shield contract — revoked/unknown/malformed/missing must all return the
SAME body, not just bodies that happen to satisfy individual assertions.
This commit is contained in:
Jannis Braun
2026-04-28 20:46:24 +02:00
parent effb36c7f4
commit 1b76b8bf2a
+17 -24
View File
@@ -160,10 +160,10 @@ describe('GET /api/auth/check-invite', () => {
url: `/api/auth/check-invite?token=${token}`, url: `/api/auth/check-invite?token=${token}`,
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const body = res.json(); // toEqual locks the byte-identical-response contract: the enumeration
expect(body.valid).toBe(false); // shield depends on revoked/unknown/malformed all returning the SAME
expect(body.reason).toBe('invalid'); // body, not just bodies that happen to satisfy individual assertions.
expect(body.name).toBeUndefined(); expect(res.json()).toEqual({ valid: false, reason: 'invalid' });
}); });
it("returns valid: false, reason: 'invalid' for unknown token", async () => { it("returns valid: false, reason: 'invalid' for unknown token", async () => {
@@ -174,10 +174,7 @@ describe('GET /api/auth/check-invite', () => {
url: `/api/auth/check-invite?token=${token}`, url: `/api/auth/check-invite?token=${token}`,
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const body = res.json(); expect(res.json()).toEqual({ valid: false, reason: 'invalid' });
expect(body.valid).toBe(false);
expect(body.reason).toBe('invalid');
expect(body.name).toBeUndefined();
}); });
it("returns valid: false, reason: 'invalid' for malformed token", async () => { it("returns valid: false, reason: 'invalid' for malformed token", async () => {
@@ -186,27 +183,23 @@ describe('GET /api/auth/check-invite', () => {
url: '/api/auth/check-invite?token=tooshort', url: '/api/auth/check-invite?token=tooshort',
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const body = res.json(); expect(res.json()).toEqual({ valid: false, reason: 'invalid' });
expect(body.valid).toBe(false);
expect(body.reason).toBe('invalid');
expect(body.name).toBeUndefined();
}); });
it('does not include name field on invalid responses', async () => { it('returns byte-identical bodies across all invalid permutations', async () => {
// Sweep across all invalid permutations to assert the absence-of-leak // The enumeration shield depends on revoked/unknown/malformed/missing
// contract once for the whole endpoint, not just per-status. // all returning the SAME body. Object equality (toEqual) catches any
// future code path that adds an extra field on one branch but not others.
const cases = [ const cases = [
{ url: '/api/auth/check-invite' }, '/api/auth/check-invite',
{ url: '/api/auth/check-invite?token=' }, '/api/auth/check-invite?token=',
{ url: '/api/auth/check-invite?token=tooshort' }, '/api/auth/check-invite?token=tooshort',
{ url: `/api/auth/check-invite?token=${'Z'.repeat(22)}` }, `/api/auth/check-invite?token=${'Z'.repeat(22)}`,
]; ];
for (const c of cases) { for (const url of cases) {
const res = await app.inject({ method: 'GET', url: c.url }); const res = await app.inject({ method: 'GET', url });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const body = res.json(); expect(res.json()).toEqual({ valid: false, reason: 'invalid' });
expect(body.valid).toBe(false);
expect(body).not.toHaveProperty('name');
} }
}); });
}); });