const assert = require('node:assert/strict'); const http = require('node:http'); const test = require('node:test'); const jwt = require('jsonwebtoken'); const app = require('../app'); const db = require('../src/config/db'); const JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret'; process.env.JWT_SECRET = JWT_SECRET; let originalQuery; const createToken = (payload = {}) => jwt.sign( { id: 1, dni: '58045340X', id_proveedor: 675, ...payload }, JWT_SECRET, { expiresIn: '1h' } ); const withServer = async (callback) => new Promise((resolve, reject) => { const server = app.listen(0, '127.0.0.1'); server.on('error', reject); server.on('listening', async () => { try { const result = await callback(server); server.close((closeError) => { if (closeError) { reject(closeError); return; } resolve(result); }); } catch (error) { server.close(() => reject(error)); } }); }); const requestJson = async ({ port, path, authorization }) => new Promise((resolve, reject) => { const req = http.request( { hostname: '127.0.0.1', port, method: 'GET', path, headers: authorization ? { authorization } : {} }, (res) => { let rawBody = ''; res.on('data', (chunk) => { rawBody += chunk; }); res.on('end', () => { const body = rawBody ? JSON.parse(rawBody) : null; resolve({ statusCode: res.statusCode, body }); }); } ); req.on('error', reject); req.end(); }); test.before(() => { originalQuery = db.query; }); test.after(() => { db.query = originalQuery; }); test('GET /api/trips está registrado en /api', () => { const apiRouterLayers = app._router.stack.filter( (layer) => layer.name === 'router' && layer.regexp && layer.regexp.toString().includes('^\\/api\\/?(?=\\/|$)') ); assert.ok(apiRouterLayers.length > 0, 'Router /api is not mounted'); const tripsRouteLayer = apiRouterLayers .flatMap((routerLayer) => routerLayer.handle.stack) .find( (layer) => layer.route && layer.route.path === '/trips' && layer.route.methods.get ); assert.ok(tripsRouteLayer, 'GET /api/trips route is not defined'); }); test('GET /api/trips devuelve viajes del transportista autenticado con aliases legacy e incluye asignados', async () => { const mockedTrips = [ { id_viaje: 84919, cod_viaje: 'VIA-2026-0001', id_estado: 7, nombrea: 'Barcelona, ES', nombreb: 'Lyon, FR', fecha_salida: '2026-01-22 06:00:00', fecha_llegada: '2026-01-22 18:11:00' }, { id_viaje: 84918, cod_viaje: 'VIA-2026-0000', id_estado: 1, nombrea: 'Madrid, ES', nombreb: 'Bilbao, ES', fecha_salida: '2026-01-21', fecha_llegada: '2026-01-21' } ]; let callCount = 0; db.query = async (sql, params) => { callCount += 1; assert.match(sql, /FROM c_viajes_proveedor p/); assert.match(sql, /INNER JOIN c_viajes v/); assert.match(sql, /INNER JOIN m_proveedores_trasportistas t/); assert.match(sql, /id_estado IN \(\?, \?, \?, \?\)/); if (callCount === 1) { assert.match(sql, /COUNT\(\*\) AS total/); assert.deepEqual(params, ['58045340X', 7, 8, 9, 1]); return [[{ total: 2 }]]; } assert.match(sql, /AS id_viaje/); assert.match(sql, /AS id_estado/); assert.match(sql, /AS nombrea/); assert.match(sql, /AS nombreb/); assert.match(sql, /AS fecha_salida/); assert.match(sql, /AS fecha_llegada/); assert.match(sql, /LIMIT \? OFFSET \?/); assert.deepEqual(params, ['58045340X', 7, 8, 9, 1, 25, 0]); return [mockedTrips]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.deepEqual(response.body, { trips: mockedTrips, page: 1, limit: 25, total: 2, has_more: false }); assert.equal(response.body.trips[0].id_estado, 7); assert.equal(response.body.trips[1].id_estado, 1); }); test('GET /api/trips devuelve lista vacia cuando no hay viajes', async () => { let callCount = 0; db.query = async () => { callCount += 1; return callCount === 1 ? [[{ total: 0 }]] : [[]]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.deepEqual(response.body, { trips: [], page: 1, limit: 25, total: 0, has_more: false }); }); test('GET /api/trips responde en menos de 1s para 25 viajes mockeados', async () => { const mockedTrips = Array.from({ length: 25 }, (_, index) => ({ id_viaje: 100000 + index, cod_viaje: `VIA-2026-${String(index + 1).padStart(4, '0')}`, id_estado: index % 2 === 0 ? 7 : 4, nombrea: `ORIGEN ${index + 1}`, nombreb: `DESTINO ${index + 1}`, fecha_salida: '2026-01-22 06:00:00', fecha_llegada: '2026-01-22 18:11:00' })); let callCount = 0; db.query = async () => { callCount += 1; return callCount === 1 ? [[{ total: 500 }]] : [mockedTrips]; }; const startedAt = Date.now(); const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips', authorization: `Bearer ${createToken()}` }) ); const elapsedMs = Date.now() - startedAt; assert.equal(response.statusCode, 200); assert.equal(response.body.trips.length, 25); assert.equal(response.body.total, 500); assert.equal(response.body.has_more, true); assert.ok(elapsedMs < 1000, `Expected < 1000ms, got ${elapsedMs}ms`); }); test('GET /api/trips aplica paginacion y filtros en SQL', async () => { const mockedTrips = [ { id_viaje: 84919, cod_viaje: 'VIA-2026-0001', id_estado: 7, nombrea: 'Barcelona, ES', nombreb: 'Lyon, FR', fecha_salida: '2026-06-30 06:00:00', fecha_llegada: '2026-06-30 18:11:00' } ]; let callCount = 0; db.query = async (sql, params) => { callCount += 1; assert.match(sql, /v\.id_estado IN \(\?, \?, \?\)/); assert.match(sql, /COALESCE\(p\.fecha_salida, v\.fecha_salida\) >= \?/); assert.match(sql, /COALESCE\(p\.fecha_salida, v\.fecha_salida\) < \?/); if (callCount === 1) { assert.match(sql, /COUNT\(\*\) AS total/); assert.deepEqual(params, ['58045340X', 7, 8, 9, '2026-06-01', '2026-07-01']); return [[{ total: 26 }]]; } assert.match(sql, /ORDER BY COALESCE\(p\.fecha_salida, v\.fecha_salida\) DESC, p\.id_viaje DESC/); assert.match(sql, /LIMIT \? OFFSET \?/); assert.deepEqual(params, ['58045340X', 7, 8, 9, '2026-06-01', '2026-07-01', 25, 25]); return [mockedTrips]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips?page=2&limit=25&status_ids=7,8,9&date_from=2026-06-01&date_to=2026-06-30', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.equal(response.body.page, 2); assert.equal(response.body.limit, 25); assert.equal(response.body.total, 26); assert.equal(response.body.has_more, false); assert.deepEqual(response.body.trips, mockedTrips); }); test('GET /api/trips valida parametros invalidos', async () => { db.query = async () => { throw new Error('db.query should not be called with invalid params'); }; const invalidPaths = [ '/api/trips?page=0', '/api/trips?limit=101', '/api/trips?status_ids=7,x', '/api/trips?date_from=2026-02-30', '/api/trips?date_from=2026-07-01&date_to=2026-06-30' ]; for (const path of invalidPaths) { const response = await withServer(async (server) => requestJson({ port: server.address().port, path, authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 400, path); assert.equal(response.body.success, false); } }); test('GET /api/trips devuelve 401 sin token', async () => { db.query = async () => { throw new Error('db.query should not be called without token'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips' }) ); assert.equal(response.statusCode, 401); assert.deepEqual(response.body, { error: 'Unauthorized' }); }); test('GET /api/trips devuelve 500 en error interno', async () => { db.query = async () => { throw new Error('forced db failure'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 500); assert.deepEqual(response.body, { success: false, error: 'Internal server error' }); });