/** * 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({ 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({ 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({ 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') }) })