feat(04-03): implement listsRouter POST/GET/PATCH/DELETE /api/lists (LIST-01)

- GET /: scoped access (owner + list_shares); activeCount/doneCount per list
- POST /: auto-populates list_shares for all other members when isShared=true (D-01/D-02)
- PATCH /🆔 rename + isShared toggle; reconciles list_shares on visibility change
- DELETE /🆔 owner-only; cascade handles items/shares via FK onDelete cascade
- resolveUserId helper copied verbatim from events.ts per project convention
- zod createListSchema (name 1..255, isShared default true) + patchListSchema
- T-04-02 / T-04-05 / T-04-07 / T-04-08 mitigations applied
- listsRouter mounted at /api/lists in index.ts (after sseRouter)
- Plan 06 SSE seam comments left at every mutation handler
- [Rule 1 - Fix] zValidator returns 400 (not 422); tests corrected to match convention
- All 23 tests green; full API suite 140 passed no regressions
This commit is contained in:
Lucas Berger
2026-06-09 12:38:05 -04:00
parent 2b3d7896f1
commit 9546b747d2
3 changed files with 407 additions and 6 deletions
+8 -6
View File
@@ -237,7 +237,7 @@ describe('POST /api/lists — create list (LIST-01, D-01)', () => {
expect(body.isShared).toBe(true)
})
it('rejects a name longer than 255 characters with 422', async () => {
it('rejects a name longer than 255 characters with 400 (zod validation)', async () => {
const userId = await seedUser('post-long-name')
currentDevUserId = userId
@@ -245,10 +245,11 @@ describe('POST /api/lists — create list (LIST-01, D-01)', () => {
const res = await app.request(
jsonRequest('POST', '/api/lists', { name: 'a'.repeat(256), isShared: true }),
)
expect(res.status).toBe(422)
// @hono/zod-validator returns 400 on schema violations (matches events.ts convention)
expect(res.status).toBe(400)
})
it('rejects an empty name with 422', async () => {
it('rejects an empty name with 400 (zod validation)', async () => {
const userId = await seedUser('post-empty-name')
currentDevUserId = userId
@@ -256,7 +257,7 @@ describe('POST /api/lists — create list (LIST-01, D-01)', () => {
const res = await app.request(
jsonRequest('POST', '/api/lists', { name: '', isShared: true }),
)
expect(res.status).toBe(422)
expect(res.status).toBe(400)
})
it('returns 401 when no session is set', async () => {
@@ -420,7 +421,8 @@ describe('PATCH /api/lists/:id (authorized update)', () => {
const app = await getApp()
const res = await app.request(jsonRequest('PATCH', `/api/lists/${listId}`, {}))
expect(res.status).toBe(422)
// @hono/zod-validator returns 400 on schema violations (matches events.ts convention)
expect(res.status).toBe(400)
})
it('zod rejects a name longer than 255 characters (T-04-07)', async () => {
@@ -430,7 +432,7 @@ describe('PATCH /api/lists/:id (authorized update)', () => {
const app = await getApp()
const res = await app.request(jsonRequest('PATCH', `/api/lists/${listId}`, { name: 'a'.repeat(256) }))
expect(res.status).toBe(422)
expect(res.status).toBe(400)
})
it('sharee can rename a shared list they have access to', async () => {