- F21-01: plan_ready and list_ready links carry their week (/plan?week=, /plan/shopping?week=), and the Plan opens on ?week= and keeps it on the week shown, so an older update opens its week. - F21-02: a failed refresh of the Rotation check shows its ErrorBanner beside the cached pending card instead of replacing it. - F21-03: Home carries the household's timezone and is read again at its next local midnight. - F21-04: Home counts pending Side quests through a read-only query (suggest.PendingCount), so reading Home never writes batches. Task: ac3dddf0-a05f-4e43-a5ad-3105fe6bdd95 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { HOME } from '../test/ApiProviders.tsx'
|
|
import { parseHome, parseUpdates } from './api.ts'
|
|
|
|
const origin = 'https://mealquest.example'
|
|
const where = 'GET /api/v1/updates'
|
|
|
|
// body is a GET /api/v1/updates body holding one update that opens url.
|
|
function body(url: string) {
|
|
return {
|
|
updates: [{ type: 'plan_ready', title: 'Your week is drafted', body: '', url, created_at: '2026-10-10T16:00:00.000Z' }],
|
|
}
|
|
}
|
|
|
|
describe('parseUpdates', () => {
|
|
it('keeps a same-origin path with its query and fragment, as parsed', () => {
|
|
expect(parseUpdates(where, body('/plan/shopping?week=2026-10-05#tuesday'), origin)[0]!.url).toBe(
|
|
'/plan/shopping?week=2026-10-05#tuesday',
|
|
)
|
|
expect(parseUpdates(where, body('/plan/../discover'), origin)[0]!.url).toBe('/discover')
|
|
})
|
|
|
|
// Each of these would open another site, or run script, once parsed: URL
|
|
// parsing strips tabs and newlines and reads a backslash as a slash.
|
|
it.each([
|
|
['a tab', '/\t/evil.example/x'],
|
|
['a newline', '/\n/evil.example/x'],
|
|
['a carriage return', '/\r/evil.example/x'],
|
|
['a backslash', '/\\evil.example/x'],
|
|
['dot segments', '/..//evil.example/x'],
|
|
['a protocol-relative URL', '//evil.example/x'],
|
|
['an absolute URL', 'https://evil.example/x'],
|
|
['javascript:', 'javascript:alert(1)'],
|
|
['a relative path', 'plan'],
|
|
])('refuses an update url with %s', (_, url) => {
|
|
expect(() => parseUpdates(where, body(url), origin)).toThrow(
|
|
`${where}: update url ${JSON.stringify(url)} is not a path on this app`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('parseHome', () => {
|
|
it("keeps the household's zone, and refuses one that is not an IANA zone", () => {
|
|
expect(parseHome('GET /api/v1/home', HOME).timezone).toBe('America/New_York')
|
|
expect(() => parseHome('GET /api/v1/home', { ...HOME, timezone: 'Mars/Olympus' })).toThrow(
|
|
'GET /api/v1/home: timezone "Mars/Olympus" is not an IANA zone',
|
|
)
|
|
expect(() => parseHome('GET /api/v1/home', { ...HOME, timezone: undefined })).toThrow(
|
|
'GET /api/v1/home: timezone undefined is not an IANA zone',
|
|
)
|
|
})
|
|
})
|