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 }); test('reads are public; writes require the admin token', async () => { const app = await make(); let r = await app.inject({ method: 'GET', url: '/api/labels' }); assert.equal(r.statusCode, 200); assert.deepEqual(r.json().labels, []); r = await app.inject({ method: 'PUT', url: '/api/labels/113165', payload: { kind: 'free_2h' } }); assert.equal(r.statusCode, 401); r = await app.inject({ method: 'PUT', url: '/api/labels/113165', headers: auth, payload: { kind: 'free_2h', zoneName: 'DL', customerId: 217 }, }); assert.equal(r.statusCode, 200); assert.equal(r.json().kind, 'free_2h'); assert.equal(r.json().zoneName, 'DL'); assert.equal(r.json().customerId, '217'); r = await app.inject({ method: 'GET', url: '/api/labels/113165' }); assert.equal(r.statusCode, 200); assert.equal(r.json().kind, 'free_2h'); await app.close(); }); test('upsert replaces the kind', async () => { const app = await make(); await app.inject({ method: 'PUT', url: '/api/labels/1', headers: auth, payload: { kind: 'free_2h' } }); await app.inject({ method: 'PUT', url: '/api/labels/1', headers: auth, payload: { kind: 'pay_immediate' } }); const r = await app.inject({ method: 'GET', url: '/api/labels/1' }); assert.equal(r.json().kind, 'pay_immediate'); await app.close(); }); test('invalid kind is rejected', async () => { const app = await make(); const r = await app.inject({ method: 'PUT', url: '/api/labels/1', headers: auth, payload: { kind: 'free_9h' } }); assert.equal(r.statusCode, 400); assert.equal(r.json().error, 'bad_kind'); await app.close(); }); test('unknown zone → 404 on GET and DELETE', async () => { const app = await make(); assert.equal((await app.inject({ method: 'GET', url: '/api/labels/nope' })).statusCode, 404); assert.equal((await app.inject({ method: 'DELETE', url: '/api/labels/nope', headers: auth })).statusCode, 404); await app.close(); }); test('delete removes the label', async () => { const app = await make(); await app.inject({ method: 'PUT', url: '/api/labels/9', headers: auth, payload: { kind: 'free_4h' } }); assert.equal((await app.inject({ method: 'DELETE', url: '/api/labels/9', headers: auth })).statusCode, 200); assert.equal((await app.inject({ method: 'GET', url: '/api/labels/9' })).statusCode, 404); await app.close(); }); test('whoami reflects the token', async () => { const app = await make(); assert.equal((await app.inject({ method: 'GET', url: '/api/whoami' })).statusCode, 401); const ok = await app.inject({ method: 'GET', url: '/api/whoami', headers: auth }); assert.equal(ok.statusCode, 200); assert.equal(ok.json().admin, true); await app.close(); }); test('repeated auth failures auto-block the IP (403)', async () => { const app = await make({ authFailLimit: 3, blockCooldownMs: 60_000 }); for (let i = 0; i < 3; i++) { await app.inject({ method: 'PUT', url: '/api/labels/1', headers: { authorization: 'Bearer wrong' }, payload: { kind: 'free_2h' }, }); } const r = await app.inject({ method: 'GET', url: '/api/labels' }); // even a public read is now blocked assert.equal(r.statusCode, 403); await app.close(); }); test('rate limit returns 429 past the threshold', async () => { const app = await make({ rateLimitMax: 3, rateLimitWindow: '1 minute' }); let last; for (let i = 0; i < 5; i++) last = await app.inject({ method: 'GET', url: '/api/labels' }); assert.equal(last!.statusCode, 429); await app.close(); }); test('zone mirror: admin sync then public read', async () => { const app = await make(); // sync requires auth let r = await app.inject({ method: 'POST', url: '/api/zones/sync', payload: { zones: [{ ZoneId: 113165, ZoneName: 'DL', Lat: 48.27, Long: -116.55 }] }, }); assert.equal(r.statusCode, 401); // authed sync (one zone lacks ZoneId → skipped) r = await app.inject({ method: 'POST', url: '/api/zones/sync', headers: auth, payload: { zones: [ { ZoneId: 113165, ZoneName: 'DL', Lat: 48.27, Long: -116.55, Spaces: [{ SpaceId: 1 }] }, { ZoneName: 'no-id' }, ], }, }); assert.equal(r.statusCode, 200); assert.equal(r.json().synced, 1); // public read returns full Zone objects r = await app.inject({ method: 'GET', url: '/api/zones' }); assert.equal(r.statusCode, 200); assert.equal(r.json().count, 1); assert.equal(r.json().zones[0].ZoneName, 'DL'); assert.equal(r.json().zones[0].Spaces[0].SpaceId, 1); await app.close(); }); test('seeded IP denylist blocks with 403', async () => { // app.inject uses 127.0.0.1 as the client IP const app = await make({ seedBlockedIps: ['127.0.0.1'] }); assert.equal((await app.inject({ method: 'GET', url: '/api/labels' })).statusCode, 403); await app.close(); });