feat(04-01): install new deps + scaffold API test harness with Wave-0 RED stubs

- Add react-router@7, @dnd-kit/core, @dnd-kit/sortable, fractional-indexing to PWA
- Add fractional-indexing to API (rank generation server-side)
- ioredis NOT added (in-memory EventEmitter per RESEARCH Plan 02 justification)
- Create apps/api/test/setup.ts with afterEach DB cleanup for list tables
- Wire test.setupFiles in apps/api/vitest.config.ts
- Add 4 Wave-0 RED stub test files (LIST-01/02/03/04, D-04, D-11, D-07)
- All stubs run as todo, not import-error
This commit is contained in:
Lucas Berger
2026-06-09 11:49:10 -04:00
parent 95b13e6633
commit 39d4ec84c0
9 changed files with 241 additions and 2 deletions
+19
View File
@@ -0,0 +1,19 @@
/**
* Wave-0 RED stubs for listEmitter (in-memory EventEmitter fan-out).
*
* Covers D-04: SSE fan-out MUST be scoped to who can see a list.
* These are pure-unit tests — no DB or HTTP server needed.
*
* Downstream plans implement the actual listEmitter.ts module that these stubs test.
*
* Run: pnpm --filter @familysync/api test
*/
describe('listEmitter — scoped fan-out correctness (D-04)', () => {
it.todo('publishListEvent emits only to subscribers for the matching listId')
it.todo('publishListEvent does NOT emit to subscribers for a different listId')
it.todo('subscribeListEvents returns an unsubscribe function that stops future events')
it.todo('unsubscribed handler is not called after unsubscribe()')
it.todo('multiple subscribers for the same listId all receive the event')
it.todo('emitter handles 100+ concurrent subscribers without error (D-18 scale check)')
})
+42
View File
@@ -0,0 +1,42 @@
/**
* Wave-0 RED stubs for the lists API router.
*
* These stubs cover LIST-01 through LIST-04 behavior.
* Downstream plans (02/03/06) replace the `it.todo` entries with real assertions
* once the routes and DB are wired.
*
* Security focus: T-04-02 — scoped fan-out (private list events must NOT reach non-owner).
*
* Run: pnpm --filter @familysync/api test
*/
describe('GET /api/lists — LIST-01: returns only accessible lists', () => {
it.todo('returns empty array when user has no lists')
it.todo('returns lists owned by the current user')
it.todo('returns lists shared with the current user via list_shares')
it.todo('does NOT return private lists owned by another user')
})
describe('POST /api/lists — LIST-01: create list', () => {
it.todo('creates a list and inserts a list_shares row when isShared=true (D-01)')
it.todo('creates a private list with no list_shares row when isShared=false')
it.todo('rejects a name longer than 255 characters with 422')
it.todo('returns 401 when called without a session')
})
describe('PATCH /api/list-items/:id — LIST-02: per-field update', () => {
it.todo('updates only the checked field when patch body is { checked: true } (D-08)')
it.todo('updates only the text field when patch body is { text: "..." } (D-08)')
it.todo('rejects a patch body with more than one field with 422')
it.todo('returns 403 when caller does not own or share the parent list (T-04-02)')
})
describe('PATCH /api/list-items/:id rank — LIST-03: fractional reorder', () => {
it.todo('updates the rank field to the new fractional-indexing string')
it.todo('rejects an empty rank string with 422')
})
describe('SSE scoped fan-out — LIST-04 / T-04-02: private-list event isolation', () => {
it.todo('publishListEvent on a private list does NOT emit to a subscriber for a different list')
it.todo('publishListEvent on a shared list emits to all subscribers for that list')
})