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:
@@ -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",
|
||||
|
||||
@@ -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)')
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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.
|
||||
}
|
||||
})
|
||||
@@ -4,5 +4,6 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
setupFiles: ['./test/setup.ts'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user