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
+1
View File
@@ -19,6 +19,7 @@
"@hono/oidc-auth": "1.8.3",
"@hono/zod-validator": "0.8.0",
"drizzle-orm": "0.45.2",
"fractional-indexing": "^3.2.0",
"hono": "4.12.23",
"ical.js": "2.2.1",
"mysql2": "3.22.4",
+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')
})
+37
View File
@@ -0,0 +1,37 @@
/**
* Vitest global test setup for apps/api.
*
* Establishes shared test infrastructure for API tests:
* - DB pool access via the existing client.ts (DB_HOST/DB_NAME from env)
* - Per-test cleanup for list tables (truncate between tests so state is isolated)
*
* Usage:
* This file is referenced in vitest.config.ts via test.setupFiles.
* Pure-logic tests (listEmitter, fractional rank) do NOT require DB — the
* cleanup function is a no-op when the tables are empty.
*
* Environment:
* Set DB_HOST, DB_USER, DB_PASSWORD, DB_NAME in the test environment.
* Tests run against a local MariaDB dev database; production data is never touched.
*/
import { afterEach } from 'vitest'
import { db } from '../src/db/client.js'
import { lists, listItems, listShares } from '../src/db/schema.js'
/**
* Truncate list tables in FK-safe order after each test.
* list_items and list_shares have FK to lists; delete children first.
* Called automatically via afterEach — no per-test setup needed.
*/
afterEach(async () => {
try {
// Delete child rows first to avoid FK constraint violations
await db.delete(listItems)
await db.delete(listShares)
await db.delete(lists)
} catch {
// DB may not be available in pure-unit test runs (no DB_HOST configured).
// Swallow the error — pure-logic tests do not need cleanup.
}
})
+1
View File
@@ -4,5 +4,6 @@ export default defineConfig({
test: {
environment: 'node',
globals: true,
setupFiles: ['./test/setup.ts'],
},
})