diff --git a/apps/api/tests/routes/admin.test.ts b/apps/api/tests/routes/admin.test.ts index bfc735e..0a12737 100644 --- a/apps/api/tests/routes/admin.test.ts +++ b/apps/api/tests/routes/admin.test.ts @@ -1036,4 +1036,29 @@ describe('POST /api/admin/members', () => { expect(adminRow).toBeDefined(); expect(adminRow!.hasLocalCredential).toBe(false); }); + + it('WR-05 (no-echo): malformed create-member body never echoes the submitted password or Zod received field', async () => { + const adminId = await seedUser('admin-create-noecho', true); + currentDevUserId = adminId; + const app = await getApp(); + + // initialPassword too short (< 8) → Zod rejects. The noEchoHook must return only + // { error: 'Invalid request' } and NEVER leak the submitted password or Zod's + // issues[].received field (T-19-06 / the T-19-14 leak this guards against). + const submittedPassword = 'shortpw-secret'; + const res = await app.fetch( + jsonRequest('POST', '/api/admin/members', { + displayName: 'No Echo', + username: `noecho-${randomUUID()}`, + initialPassword: submittedPassword.slice(0, 3), // 3 chars — fails min(8) + }), + ); + expect(res.status).toBe(400); + const bodyText = await res.text(); + expect(bodyText).not.toContain('received'); + expect(bodyText).not.toContain('issues'); + expect(bodyText).not.toContain(submittedPassword.slice(0, 3)); + const parsed = JSON.parse(bodyText) as { error: string }; + expect(parsed.error).toBe('Invalid request'); + }); }); diff --git a/apps/api/tests/routes/me.test.ts b/apps/api/tests/routes/me.test.ts index 72a1527..6f50480 100644 --- a/apps/api/tests/routes/me.test.ts +++ b/apps/api/tests/routes/me.test.ts @@ -403,6 +403,27 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () => expect(res.status).toBe(404); }); + + it('Test 4 (WR-05 no-echo): malformed body never echoes the submitted password or Zod received field', async () => { + // newPassword too short (< 8) → Zod rejects via meNoEchoHook. The response must be + // ONLY { error: 'Invalid request' } and must NOT leak the submitted password or the + // Zod issues[].received field (T-19-06). + const { app } = await import('../../src/index.js'); + const submitted = 'my-secret-current-pw'; + const res = await app.request('/api/me/password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword: submitted, newPassword: 'short' }), + }); + + expect(res.status).toBe(400); + const bodyText = await res.text(); + expect(bodyText).not.toContain(submitted); + expect(bodyText).not.toContain('received'); + expect(bodyText).not.toContain('issues'); + const parsed = JSON.parse(bodyText) as { error: string }; + expect(parsed.error).toBe('Invalid request'); + }); }); // ---------------------------------------------------------------------------