import { test } from 'node:test'; import assert from 'node:assert/strict'; import { buildApp, type BuildOptions } from '../src/app.ts'; const TOKEN = 'test-admin-token-0123456789'; const auth = { authorization: `Bearer ${TOKEN}` }; const make = (o: Partial = {}) => buildApp({ adminToken: TOKEN, dbPath: ':memory:', ...o }); const line = (id: string, kind = 'free_2h') => ({ id, kind, name: `${id} name`, label: '2-hour free', legend: 'Permits not valid', hours: 2, color: '#d367cc', geometry: { type: 'LineString', coordinates: [ [-116.5535, 48.2766], [-116.5525, 48.2766], ], }, }); test('areas are public to read and admin-only to replace', async () => { const app = await make(); let r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.equal(r.statusCode, 200); assert.deepEqual(r.json().areas, []); assert.equal(r.json().count, 0); r = await app.inject({ method: 'PUT', url: '/api/areas', payload: { areas: [line('sp-001')] } }); assert.equal(r.statusCode, 401); r = await app.inject({ method: 'PUT', url: '/api/areas', headers: auth, payload: { areas: [line('sp-001'), line('sp-002', 'limit_3h')] }, }); assert.equal(r.statusCode, 200); assert.equal(r.json().replaced, 2); r = await app.inject({ method: 'GET', url: '/api/areas' }); const areas = r.json().areas; assert.equal(areas.length, 2); assert.equal(areas[0].id, 'sp-001'); assert.equal(areas[0].kind, 'free_2h'); assert.equal(areas[0].shape, 'line'); // Geometry survives the round-trip as real GeoJSON, not a string. assert.deepEqual(areas[0].geometry.coordinates[0], [-116.5535, 48.2766]); await app.close(); }); test('replace is wholesale — stale areas do not survive', async () => { const app = await make(); await app.inject({ method: 'PUT', url: '/api/areas', headers: auth, payload: { areas: [line('sp-001'), line('sp-002')] }, }); await app.inject({ method: 'PUT', url: '/api/areas', headers: auth, payload: { areas: [line('sp-003')] }, }); const r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.equal(r.json().count, 1); assert.equal(r.json().areas[0].id, 'sp-003'); await app.close(); }); test('polygons are accepted; bad kinds and geometries are rejected', async () => { const app = await make(); const lot = { id: 'sp-022', kind: 'green_lot', name: 'Lot off Oak St', label: 'City lot', legend: 'Paid hourly or permit', hours: 2, color: '#75b259', geometry: { type: 'Polygon', coordinates: [ [ [-116.554, 48.2766], [-116.553, 48.2766], [-116.553, 48.2772], [-116.554, 48.2766], ], ], }, }; let r = await app.inject({ method: 'PUT', url: '/api/areas', headers: auth, payload: { areas: [lot] }, }); assert.equal(r.statusCode, 200); assert.equal(r.json().replaced, 1); r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.equal(r.json().areas[0].shape, 'polygon'); r = await app.inject({ method: 'PUT', url: '/api/areas', headers: auth, payload: { areas: [{ ...line('sp-009'), kind: 'free_9h' }] }, }); assert.equal(r.statusCode, 400); assert.equal(r.json().error, 'bad_kind'); r = await app.inject({ method: 'PUT', url: '/api/areas', headers: auth, payload: { areas: [{ ...line('sp-009'), geometry: { type: 'Point', coordinates: [0, 0] } }] }, }); assert.equal(r.statusCode, 400); assert.equal(r.json().error, 'bad_geometry'); // A rejected batch must not have clobbered the good one. r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.equal(r.json().count, 1); assert.equal(r.json().areas[0].id, 'sp-022'); await app.close(); }); test('overlay defaults to identity and round-trips', async () => { const app = await make(); let r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.deepEqual(r.json().overlay, { dxMeters: 0, dyMeters: 0, scale: 1, rotationDeg: 0, updatedAt: 0, }); r = await app.inject({ method: 'PUT', url: '/api/areas/overlay', payload: { dxMeters: 3 }, }); assert.equal(r.statusCode, 401); r = await app.inject({ method: 'PUT', url: '/api/areas/overlay', headers: auth, payload: { dxMeters: 3.5, dyMeters: -2, scale: 1.01, rotationDeg: 0.4 }, }); assert.equal(r.statusCode, 200); assert.equal(r.json().dxMeters, 3.5); assert.ok(r.json().updatedAt > 0); r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.equal(r.json().overlay.dyMeters, -2); assert.equal(r.json().overlay.rotationDeg, 0.4); await app.close(); }); test('an absurd overlay scale is refused rather than stored', async () => { const app = await make(); for (const scale of [0, 0.4, 2, 5]) { const r = await app.inject({ method: 'PUT', url: '/api/areas/overlay', headers: auth, payload: { scale }, }); assert.equal(r.statusCode, 400, `scale ${scale} should be refused`); } const r = await app.inject({ method: 'GET', url: '/api/areas' }); assert.equal(r.json().overlay.scale, 1); await app.close(); });