import test from 'node:test'; import assert from 'node:assert/strict'; import { openDb } from '../src/db.mjs'; import { createRouter } from '../src/server/router.mjs'; import { registerCaptureRoutes } from '../src/server/routes/capture.mjs'; import { registerExtractionRoutes } from '../src/server/routes/extractions.mjs'; function setup() { const db = openDb(':memory:'); db.prepare("INSERT INTO municipios (codigo_ibge, nome, nome_clean, uf) VALUES ('1','Salvador','salvador','BA')").run(); db.prepare("INSERT INTO segments (name,label,active) VALUES ('restaurantes','Restaurantes',1)").run(); db.prepare("INSERT INTO cnaes (codigo,descricao) VALUES ('5611201','Restaurantes')").run(); const router = createRouter(); let started; registerCaptureRoutes(router, { db, jobs: { startJob: (options) => { started = options; return 'job-1'; } } }); return { db, handler: router.match('POST', '/api/capture/runs').handler, get started() { return started; } }; } async function invoke(handler, body) { let result; await handler({}, {}, { body, json: (status, data) => { result = { status, data }; } }); return result; } const valid = { sources: ['mock'], uf: 'BA', cities: ['Salvador'], segments: ['restaurantes'], limits: { maxPerQuery: 10, queryLimit: 0, googlePages: 1 } }; test('captura aceita apenas selecoes reais e persiste parametros normalizados', async () => { const fixture = setup(); const { db, handler } = fixture; const result = await invoke(handler, valid); assert.equal(result.status, 202); assert.deepEqual(JSON.parse(db.prepare('SELECT params_json FROM capture_runs').get().params_json), valid); const row = db.prepare('SELECT job_id, owner_token, heartbeat_at FROM capture_runs').get(); assert.equal(row.job_id, 'job-1'); assert.ok(row.owner_token); assert.ok(row.heartbeat_at); assert.equal(fixture.started.ownerToken, row.owner_token); db.close(); }); test('captura rejeita arrays vazios, duplicados, excessivos e strings longas', async () => { for (const patch of [ { sources: [] }, { cities: [] }, { segments: [] }, { sources: ['mock', 'mock'] }, { sources: Array(11).fill('mock') }, { cities: Array(1001).fill('Salvador') }, { segments: Array(251).fill('restaurantes') }, { cities: ['x'.repeat(101)] } ]) { const { db, handler } = setup(); assert.equal((await invoke(handler, { ...valid, ...patch })).status, 400); db.close(); } }); test('captura rejeita cidade, segmento, CNAE e limites inexistentes ou fora da faixa', async () => { for (const patch of [ { cities: ['Feira de Santana'] }, { cities: ['all', 'Salvador'] }, { segments: ['inativo'] }, { segments: ['cnae:123'] }, { segments: ['cnae:1234567'] }, { limits: { maxPerQuery: 0, queryLimit: 0, googlePages: 1 } }, { limits: { maxPerQuery: 101, queryLimit: 0, googlePages: 1 } }, { limits: { maxPerQuery: 10, queryLimit: 10001, googlePages: 1 } }, { limits: { maxPerQuery: 10, queryLimit: 0, googlePages: 4 } } ]) { const { db, handler } = setup(); assert.equal((await invoke(handler, { ...valid, ...patch })).status, 400); db.close(); } }); test('captura converte corrida de constraint em 409 sem row parcial', async () => { const db = openDb(':memory:'); db.prepare("INSERT INTO municipios (codigo_ibge, nome, nome_clean, uf) VALUES ('1','Salvador','salvador','BA')").run(); db.prepare("INSERT INTO segments (name,label,active) VALUES ('restaurantes','Restaurantes',1)").run(); const raceDb = { prepare(sql) { if (/INSERT INTO capture_runs/.test(sql)) { const error = new Error('UNIQUE constraint failed: active capture'); error.errcode = 2067; throw error; } return db.prepare(sql); } }; const router = createRouter(); let started = false; registerCaptureRoutes(router, { db: raceDb, jobs: { startJob: () => { started = true; } } }); const result = await invoke(router.match('POST', '/api/capture/runs').handler, valid); assert.equal(result.status, 409); assert.match(result.data.erro, /simultaneamente/i); assert.equal(db.prepare('SELECT COUNT(*) total FROM capture_runs').get().total, 0); assert.equal(started, false); db.close(); }); test('extracao converte corrida de constraint em 409 sem row parcial', async () => { const db = openDb(':memory:'); const raceDb = { prepare(sql) { if (/INSERT INTO extractions/.test(sql)) { const error = new Error('UNIQUE constraint failed: active extraction'); error.errcode = 2067; throw error; } return db.prepare(sql); } }; const router = createRouter(); let started = false; registerExtractionRoutes(router, { db: raceDb, jobs: { startJob: () => { started = true; } } }); const result = await invoke(router.match('POST', '/api/extractions').handler, { uf: 'BA' }); assert.equal(result.status, 409); assert.match(result.data.erro, /simultaneamente/i); assert.equal(db.prepare('SELECT COUNT(*) total FROM extractions').get().total, 0); assert.equal(started, false); db.close(); });