// spec: e2e/plans/parent-notifications.plan.md §7 — deny chore import { test, expect, type APIRequestContext } from '@playwright/test' const CHILD_NAME = 'DigestDenyChoreChild' const CHORE_NAME = 'DigestDenyChoreChore' const CHORE_POINTS = 8 const DIGEST_ACTION_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'https://localhost:5173' async function createChild(request: APIRequestContext, name: string): Promise { const pre = await request.get('/api/child/list') for (const c of (await pre.json()).children ?? []) { if (c.name === name) await request.delete(`/api/child/${c.id}`) } await request.put('/api/child/add', { data: { name, age: 8 } }) const list = await request.get('/api/child/list') return (await list.json()).children?.find((c: any) => c.name === name)?.id ?? '' } async function createTask( request: APIRequestContext, name: string, points: number, ): Promise { const pre = await request.get('/api/task/list') for (const t of (await pre.json()).tasks ?? []) { if (t.name === name) await request.delete(`/api/task/${t.id}`) } await request.put('/api/task/add', { data: { name, points, type: 'chore' } }) const list = await request.get('/api/task/list') return (await list.json()).tasks?.find((t: any) => t.name === name)?.id ?? '' } test.describe('Digest Action Token — Deny Chore', () => { test.describe.configure({ mode: 'serial' }) let childId = '' let choreId = '' let denyToken = '' test.beforeAll(async ({ request }) => { childId = await createChild(request, CHILD_NAME) choreId = await createTask(request, CHORE_NAME, CHORE_POINTS) await request.put(`/api/child/${childId}/set-tasks`, { data: { task_ids: [choreId], type: 'chore' }, }) await request.post(`/api/child/${childId}/confirm-chore`, { data: { task_id: choreId }, }) const tokenRes = await request.post('/api/admin/test/digest-token', { data: { child_id: childId, entity_id: choreId, entity_type: 'chore', action: 'deny', }, }) expect(tokenRes.status()).toBe(200) denyToken = (await tokenRes.json()).token }) test.afterAll(async ({ request }) => { if (childId) await request.delete(`/api/child/${childId}`) if (choreId) await request.delete(`/api/task/${choreId}`) }) // 7.3 Deny-chore token returns 302 redirect to correct ParentView URL test('Deny-chore token returns 302 redirect to correct ParentView URL', async ({ playwright, }) => { const unauthCtx = await playwright.request.newContext({ ignoreHTTPSErrors: true, baseURL: DIGEST_ACTION_BASE_URL, }) const res = await unauthCtx.get(`/api/digest-action/${denyToken}`, { maxRedirects: 0, }) expect(res.status()).toBe(302) const location = res.headers()['location'] ?? '' expect(location).toContain(`/parent/${childId}`) expect(location).toContain('entityType=chore') await unauthCtx.dispose() }) // 7.4 Deny-chore action is performed: pending confirmation removed, no points awarded test('Deny-chore action is performed: confirmation removed, points unchanged', async ({ request, }) => { // Execute the action via authenticated POST (GET only redirects, POST consumes the token) const execRes = await request.post(`/api/digest-action/${denyToken}`) expect(execRes.status()).toBe(200) const childRes = await request.get(`/api/child/list`) const children = (await childRes.json()).children ?? [] const child = children.find((c: any) => c.id === childId) expect(child).toBeTruthy() // No points awarded for rejection expect(child.points).toBe(0) const pendingRes = await request.get('/api/pending-confirmations') const confirmations = (await pendingRes.json()).confirmations ?? [] const pending = confirmations.filter( (c: any) => c.child_id === childId && c.entity_id === choreId && c.status === 'pending', ) expect(pending).toHaveLength(0) }) })