ef7ea6b971
- reusable src/records/ module: useUpdate/useDelete Poo+Location hooks (encodeURIComponent PK, prefix-based query invalidation), EditPooModal, EditLocationModal, ConfirmDeleteModal — exported for the map (T09) to reuse - RecordsPage (/records): paginated poo + location tables (page size 100), edit + delete-with-confirm, refresh on success - query keys ['poo']/['locations'] so map and list invalidations cross-cut - typed client only; vitest tests
177 lines
6.8 KiB
TypeScript
177 lines
6.8 KiB
TypeScript
/**
|
|
* Real-encoding regression test for M2-T10 (REWORK 1).
|
|
*
|
|
* Motivation: RecordsPage.test.tsx mocks the entire apiClient module, so
|
|
* openapi-fetch's defaultPathSerializer never runs in those tests. That means
|
|
* the integration between hooks.ts and the real client cannot be verified there.
|
|
*
|
|
* This file uses two complementary strategies:
|
|
*
|
|
* A) Direct serializer test — import openapi-fetch's defaultPathSerializer and
|
|
* verify that raw PK values (with ':') produce single-encoded URLs (%3A,
|
|
* NOT %253A). This is a pure-function test with no network I/O.
|
|
*
|
|
* B) Live fetch stub — create a real openapi-fetch client instance with a
|
|
* custom fetch stub, call the same path that hooks.ts calls (with a raw PK),
|
|
* and assert the URL the client constructs contains exactly one level of
|
|
* encoding. This exercises the full openapi-fetch → URL-construction path.
|
|
*
|
|
* Together these prove:
|
|
* 1. openapi-fetch encodes raw ':' correctly (as '%3A', once).
|
|
* 2. The path template /api/poo/{timestamp} with a raw timestamp produces
|
|
* the right URL — and would break if encodeURIComponent were applied first.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
import createClient, { defaultPathSerializer } from 'openapi-fetch'
|
|
import type { paths } from '../api/schema.d.ts'
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A) defaultPathSerializer unit tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('openapi-fetch defaultPathSerializer (raw PK → single-encoded URL)', () => {
|
|
it('encodes a poo timestamp with colons exactly once', () => {
|
|
const template = '/api/poo/{timestamp}'
|
|
const rawTs = '2026-06-12T10:00:00Z'
|
|
const result = defaultPathSerializer(template, { timestamp: rawTs })
|
|
// Single-encoded colon
|
|
expect(result).toContain('%3A')
|
|
// Double-encoded colon must NOT appear
|
|
expect(result).not.toContain('%253A')
|
|
expect(result).toBe('/api/poo/2026-06-12T10%3A00%3A00Z')
|
|
})
|
|
|
|
it('encodes location person+datetime with colons exactly once', () => {
|
|
const template = '/api/locations/{person}/{datetime}'
|
|
const rawDt = '2026-06-12T09:00:00Z'
|
|
const result = defaultPathSerializer(template, { person: 'alice', datetime: rawDt })
|
|
expect(result).toContain('%3A')
|
|
expect(result).not.toContain('%253A')
|
|
expect(result).toBe('/api/locations/alice/2026-06-12T09%3A00%3A00Z')
|
|
})
|
|
|
|
it('pre-encoding a PK before passing it causes double-encoding (%253A)', () => {
|
|
// This test documents the BUG that was present before REWORK 1:
|
|
// hooks.ts was calling encodeURIComponent(timestamp) before passing to
|
|
// the client, so defaultPathSerializer would encode it a second time.
|
|
const template = '/api/poo/{timestamp}'
|
|
const rawTs = '2026-06-12T10:00:00Z'
|
|
const preEncoded = encodeURIComponent(rawTs) // what the old hooks.ts did
|
|
const result = defaultPathSerializer(template, { timestamp: preEncoded })
|
|
// Double-encoded: '%' → '%25', then '3A' stays → '%253A'
|
|
expect(result).toContain('%253A')
|
|
// This is WRONG — after fix, hooks must NOT pre-encode.
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// B) Live fetch-stub test using a real openapi-fetch client instance
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('real openapi-fetch client URL construction (fetch-stub)', () => {
|
|
it('DELETE /api/poo/{timestamp} with raw PK produces single-encoded URL', async () => {
|
|
const capturedUrls: string[] = []
|
|
const fakeFetch = vi.fn((_input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof _input === 'string'
|
|
? _input
|
|
: _input instanceof URL
|
|
? _input.href
|
|
: (_input as Request).url
|
|
capturedUrls.push(url)
|
|
return Promise.resolve(new Response(null, { status: 204 }))
|
|
})
|
|
|
|
// Create a real client with our fake fetch — same config as client.ts
|
|
// but with an explicit fetch override so we control the transport.
|
|
const testClient = createClient<paths>({
|
|
baseUrl: 'http://localhost/',
|
|
fetch: fakeFetch as typeof fetch,
|
|
})
|
|
|
|
const rawTs = '2026-06-12T10:00:00Z'
|
|
await testClient.DELETE('/api/poo/{timestamp}', {
|
|
params: { path: { timestamp: rawTs } },
|
|
})
|
|
|
|
expect(fakeFetch).toHaveBeenCalled()
|
|
const url = capturedUrls[0]
|
|
expect(url).toBeDefined()
|
|
|
|
// Single-encoded colon: present
|
|
expect(url).toContain('%3A')
|
|
// Double-encoded colon: must be absent
|
|
expect(url).not.toContain('%253A')
|
|
expect(url).toContain('/api/poo/2026-06-12T10%3A00%3A00Z')
|
|
})
|
|
|
|
it('DELETE /api/locations/{person}/{datetime} with raw PK produces single-encoded URL', async () => {
|
|
const capturedUrls: string[] = []
|
|
const fakeFetch = vi.fn((_input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof _input === 'string'
|
|
? _input
|
|
: _input instanceof URL
|
|
? _input.href
|
|
: (_input as Request).url
|
|
capturedUrls.push(url)
|
|
return Promise.resolve(new Response(null, { status: 204 }))
|
|
})
|
|
|
|
const testClient = createClient<paths>({
|
|
baseUrl: 'http://localhost/',
|
|
fetch: fakeFetch as typeof fetch,
|
|
})
|
|
|
|
const rawDt = '2026-06-12T09:00:00Z'
|
|
await testClient.DELETE('/api/locations/{person}/{datetime}', {
|
|
params: { path: { person: 'alice', datetime: rawDt } },
|
|
})
|
|
|
|
expect(fakeFetch).toHaveBeenCalled()
|
|
const url = capturedUrls[0]
|
|
expect(url).toBeDefined()
|
|
|
|
expect(url).toContain('%3A')
|
|
expect(url).not.toContain('%253A')
|
|
expect(url).toContain('/api/locations/alice/2026-06-12T09%3A00%3A00Z')
|
|
})
|
|
|
|
it('double-encoded PK produces wrong URL — documents the fixed bug', async () => {
|
|
// This test shows what the OLD hooks.ts would produce.
|
|
// It is intentionally asserting the BAD behavior to document the regression.
|
|
const capturedUrls: string[] = []
|
|
const fakeFetch = vi.fn((_input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof _input === 'string'
|
|
? _input
|
|
: _input instanceof URL
|
|
? _input.href
|
|
: (_input as Request).url
|
|
capturedUrls.push(url)
|
|
return Promise.resolve(new Response(null, { status: 204 }))
|
|
})
|
|
|
|
const testClient = createClient<paths>({
|
|
baseUrl: 'http://localhost/',
|
|
fetch: fakeFetch as typeof fetch,
|
|
})
|
|
|
|
const rawTs = '2026-06-12T10:00:00Z'
|
|
// Simulate what the old hooks.ts did: pre-encode before passing to client
|
|
const preEncoded = encodeURIComponent(rawTs)
|
|
await testClient.DELETE('/api/poo/{timestamp}', {
|
|
params: { path: { timestamp: preEncoded } },
|
|
})
|
|
|
|
const url = capturedUrls[0]
|
|
// The OLD code would produce double-encoding (%253A), which caused 404 on the backend
|
|
expect(url).toContain('%253A')
|
|
})
|
|
})
|