'use strict'; // Prueba de humo: crea una tabla DBF temporal, levanta el conector y valida el contrato v1. const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); const { DBFFile } = require('dbffile'); const PORT = 18455; const TOKEN = 'token-de-prueba'; async function main() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'prueba-fox-')); const data = path.join(dir, 'nomina'); fs.mkdirSync(data); const dbf = await DBFFile.create(path.join(data, 'EMPLEADO.DBF'), [ { name: 'CEDULA', type: 'C', size: 13 }, { name: 'NOMBRE', type: 'C', size: 60 }, { name: 'CARGO', type: 'C', size: 40 }, { name: 'SUELDO', type: 'N', size: 10, decimalPlaces: 2 }, { name: 'INGRESO', type: 'D', size: 8 }, { name: 'ACTIVO', type: 'L', size: 1 }, ], { encoding: 'cp1252' }); await dbf.appendRecords([ { CEDULA: '0102030405', NOMBRE: 'MUÑOZ PEÑA JOSÉ', CARGO: 'Contador', SUELDO: 1200.5, INGRESO: new Date('2020-03-15'), ACTIVO: true }, { CEDULA: '0911122233', NOMBRE: 'ANDRADE LUISA', CARGO: 'Recepción', SUELDO: 600, INGRESO: new Date('2023-01-02'), ACTIVO: true }, { CEDULA: '1700000001', NOMBRE: 'BORRADO TEST', CARGO: 'X', SUELDO: 1, INGRESO: null, ACTIVO: false }, ]); // Marcar el 3.er registro como borrado (byte '*' al inicio del registro), como hace FoxPro con DELETE. const buf = fs.readFileSync(path.join(data, 'EMPLEADO.DBF')); const headerLen = buf.readUInt16LE(8); const recLen = buf.readUInt16LE(10); buf[headerLen + recLen * 2] = 0x2a; fs.writeFileSync(path.join(data, 'EMPLEADO.DBF'), buf); fs.writeFileSync(path.join(data, 'OCULTA.DBF'), buf); const cfgPath = path.join(dir, 'config.json'); fs.writeFileSync(cfgPath, JSON.stringify({ host: '127.0.0.1', port: PORT, tokenSha256: crypto.createHash('sha256').update(TOKEN).digest('hex'), allowIps: ['127.0.0.1'], sources: [{ id: 'nomina', path: data, encoding: 'cp1252', tables: { EMPLEADO: {} }, denyFields: ['SUELDO'] }], logDir: path.join(dir, 'logs'), })); const srv = spawn(process.execPath, [path.join(__dirname, '..', 'src', 'server.js')], { env: { ...process.env, CONECTOR_CONFIG: cfgPath }, stdio: ['ignore', 'pipe', 'inherit'] }); await new Promise((resolve, reject) => { srv.stdout.once('data', resolve); srv.once('exit', (c) => reject(new Error(`el servidor salió con ${c}`))); }); const get = async (p, token = TOKEN, method = 'GET') => { const r = await fetch(`http://127.0.0.1:${PORT}${p}`, { method, headers: token ? { Authorization: `Bearer ${token}` } : {} }); return { status: r.status, body: await r.json() }; }; let ok = 0; const check = async (name, fn) => { await fn(); ok += 1; console.log(`OK ${name}`); }; try { await check('sin token → 401', async () => assert.strictEqual((await get('/v1/health', null)).status, 401)); await check('token errado → 401', async () => assert.strictEqual((await get('/v1/health', 'otro')).status, 401)); await check('POST → 405', async () => assert.strictEqual((await get('/v1/health', TOKEN, 'POST')).status, 405)); await check('health', async () => assert.strictEqual((await get('/v1/health')).body.contract, 'v1')); await check('sources', async () => assert.deepStrictEqual((await get('/v1/sources')).body.sources.map((s) => s.id), ['nomina'])); await check('tables solo permitidas', async () => { const t = (await get('/v1/sources/nomina/tables')).body.tables; assert.deepStrictEqual(t.map((x) => x.name), ['EMPLEADO']); }); await check('tabla no permitida → 404', async () => assert.strictEqual((await get('/v1/sources/nomina/tables/OCULTA/schema')).status, 404)); await check('traversal → 400', async () => assert.strictEqual((await get('/v1/sources/nomina/tables/..%2F..%2Fx/rows')).status, 400)); await check('schema sin campo denegado', async () => { const s = (await get('/v1/sources/nomina/tables/empleado/schema')).body; assert.deepStrictEqual(s.fields.map((f) => f.name), ['CEDULA', 'NOMBRE', 'CARGO', 'INGRESO', 'ACTIVO']); }); await check('rows: tildes, fecha, sin borrados', async () => { const r = (await get('/v1/sources/nomina/tables/EMPLEADO/rows')).body; assert.strictEqual(r.total, 2); assert.strictEqual(r.rows[0].NOMBRE, 'MUÑOZ PEÑA JOSÉ'); assert.strictEqual(r.rows[0].INGRESO, '2020-03-15'); assert.strictEqual(r.rows[0].SUELDO, undefined); }); await check('rows includeDeleted', async () => assert.strictEqual((await get('/v1/sources/nomina/tables/EMPLEADO/rows?includeDeleted=1')).body.total, 3)); await check('búsqueda sin tildes', async () => { const r = (await get('/v1/sources/nomina/tables/EMPLEADO/rows?q=pena%20jose&qFields=NOMBRE')).body; assert.strictEqual(r.total, 1); }); await check('eq + fields', async () => { const r = (await get('/v1/sources/nomina/tables/EMPLEADO/rows?eq.CEDULA=0911122233&fields=CEDULA,CARGO')).body; assert.deepStrictEqual(r.rows, [{ _recno: 2, CEDULA: '0911122233', CARGO: 'Recepción' }]); }); await check('pedir campo denegado → 400', async () => assert.strictEqual((await get('/v1/sources/nomina/tables/EMPLEADO/rows?fields=SUELDO')).status, 400)); await check('paginación', async () => { const r = (await get('/v1/sources/nomina/tables/EMPLEADO/rows?limit=1&offset=1')).body; assert.strictEqual(r.rows.length, 1); assert.strictEqual(r.rows[0]._recno, 2); }); await check('versión cambia al modificar la tabla', async () => { const v1 = (await get('/v1/sources/nomina/tables/EMPLEADO/schema')).body.version; const f = await DBFFile.open(path.join(data, 'EMPLEADO.DBF'), { encoding: 'cp1252' }); await f.appendRecords([{ CEDULA: '0000000001', NOMBRE: 'NUEVO', CARGO: 'Y', SUELDO: 2, INGRESO: null, ACTIVO: true }]); const r = (await get('/v1/sources/nomina/tables/EMPLEADO/rows')).body; assert.notStrictEqual(r.version, v1); assert.strictEqual(r.total, 3); }); console.log(`\n${ok} pruebas OK`); } finally { srv.kill(); fs.rmSync(dir, { recursive: true, force: true }); } } main().catch((err) => { console.error('FALLÓ:', err); process.exit(1); });