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/:id/intermediate-points 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 pointsRouteLayer = apiRouterLayers .flatMap((routerLayer) => routerLayer.handle.stack) .find( (layer) => layer.route && layer.route.path === '/trips/:id/intermediate-points' && layer.route.methods.get ); assert.ok(pointsRouteLayer, 'GET /api/trips/:id/intermediate-points route is not defined'); }); test('GET /api/trips/:id/intermediate-points devuelve lista ordenada', async () => { let step = 0; const mockedPoints = [ { id_punto_viaje: 50101, id_punto: 151, id_punto_ref: 151, posicion: 1, nombre: 'PUNTO A', contacto: 'Contacto A', telefono: '600000001', direccion: 'Direccion A', latitud: 40.416775, longitud: -3.70379, obs: 'DEVOLUCION 9 CAJAS', id_estado_intermedio: 3, estado_intermedio: 'En curso', estado_intermedio_en: 'In progress', fecha_y_hora: '2026-02-06 08:35:00', ind_fallido: 0, latitud_estado: '40.416775', longitud_estado: '-3.70379', fecha_hora: '2026-02-06 08:30:00', datetime: '2026-02-06 08:30:00' }, { id_punto_viaje: 50102, id_punto: 265, id_punto_ref: 265, posicion: 2, nombre: 'PUNTO B', contacto: 'Contacto B', telefono: '600000002', direccion: 'Direccion B', latitud: 41.385064, longitud: 2.173404, obs: 'SE NECESITA TRASPALETA', id_estado_intermedio: null, estado_intermedio: '', estado_intermedio_en: '', fecha_y_hora: null, ind_fallido: 0, latitud_estado: null, longitud_estado: null, fecha_hora: null, datetime: null } ]; db.query = async (sql, params) => { step += 1; if (step === 1) { assert.match(sql, /FROM c_viajes/); assert.deepEqual(params, [136924]); return [[{ id_viaje: 136924 }]]; } if (step === 2) { assert.match(sql, /FROM c_viajes_proveedor/); assert.deepEqual(params, [136924, '58045340X']); return [[{ authorized: 1 }]]; } assert.match(sql, /FROM c_viajes_puntos/); assert.match(sql, /LEFT JOIN m_puntos_envio_recogida/); assert.match(sql, /LEFT JOIN t_viaje_estados/); assert.match(sql, /id_estado_intermedio/); assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s+AS\s+actualizado_automaticamente/); assert.match(sql, /vp\.actualizado_automaticamente\s*=\s*1/); assert.match(sql, /vp\.fecha_hora/); assert.match(sql, /CAST\(vp\.id_punto AS SIGNED\) AS id_punto/); assert.match(sql, /AS id_punto_ref/); assert.match(sql, /NULLIF\(TRIM\(valor\), ''\) AS valor_plain/); assert.match(sql, /ELSE NULLIF\(TRIM\(valor\), ''\)/); assert.match(sql, /REGEXP '\^\[0-9\]\+\$'/); assert.match(sql, /ORDER BY vp.posicion ASC/); assert.deepEqual(params, [136924]); return [mockedPoints]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/136924/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.deepEqual(response.body, { success: true, trip_id: 136924, points: mockedPoints }); assert.equal(response.body.points.length, 2); assert.match(response.body.points[0].fecha_hora, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); assert.equal(response.body.points[0].id_punto_viaje, 50101); assert.equal(response.body.points[0].id_estado_intermedio, 3); assert.equal(response.body.points[0].estado_intermedio, 'En curso'); assert.equal(response.body.points[0].fecha_y_hora, '2026-02-06 08:35:00'); assert.equal(response.body.points[0].ind_fallido, 0); assert.equal(response.body.points[1].fecha_hora, null); assert.equal(response.body.points[1].id_estado_intermedio, null); }); test('GET /api/trips/:id/intermediate-points mantiene punto y oculta estado automatico', async () => { let step = 0; const mockedPoints = [ { id_punto_viaje: 50110, id_punto: 901, id_punto_ref: 901, posicion: 1, nombre: 'PUNTO AUTO', contacto: 'Contacto Auto', telefono: '600000099', direccion: 'Direccion Auto', latitud: 40.5, longitud: -3.6, obs: 'actualizacion automatica', id_estado_intermedio: null, estado_intermedio: '', estado_intermedio_en: '', fecha_y_hora: null, ind_fallido: null, latitud_estado: null, longitud_estado: null, fecha_hora: null, datetime: null } ]; db.query = async (sql, params) => { step += 1; if (step === 1) { return [[{ id_viaje: 136924 }]]; } if (step === 2) { return [[{ authorized: 1 }]]; } assert.match(sql, /CASE\s+WHEN vp\.actualizado_automaticamente\s*=\s*1 THEN NULL\s+ELSE vp\.id_estado_intermedio/); assert.match(sql, /CASE\s+WHEN vp\.actualizado_automaticamente\s*=\s*1 THEN ''/); assert.match(sql, /CASE\s+WHEN vp\.actualizado_automaticamente\s*=\s*1 THEN NULL\s+ELSE vp\.fecha_hora/); assert.deepEqual(params, [136924]); return [mockedPoints]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/136924/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.deepEqual(response.body, { success: true, trip_id: 136924, points: mockedPoints }); assert.equal(response.body.points.length, 1); assert.equal(response.body.points[0].id_punto, 901); assert.equal(response.body.points[0].id_estado_intermedio, null); assert.equal(response.body.points[0].estado_intermedio, ''); assert.equal(response.body.points[0].fecha_hora, null); }); test('GET /api/trips/:id/intermediate-points tolera valor texto plano antiguo como observacion', async () => { let step = 0; const mockedPoints = [ { id_punto_viaje: 50120, id_punto: null, id_punto_ref: null, posicion: 1, nombre: '', contacto: '', telefono: '', direccion: '', latitud: null, longitud: null, obs: 'texto plano legado', id_estado_intermedio: 3, estado_intermedio: 'Posicionado', estado_intermedio_en: 'Positioned', fecha_y_hora: '2026-03-10 09:00:15', ind_fallido: 0, latitud_estado: '40.532405853271484', longitud_estado: '-3.307368516921997', fecha_hora: '2026-03-10 09:00:15', datetime: '2026-03-10 09:00:15' } ]; db.query = async (sql, params) => { step += 1; if (step === 1) { return [[{ id_viaje: 136924 }]]; } if (step === 2) { return [[{ authorized: 1 }]]; } assert.match(sql, /ELSE NULLIF\(TRIM\(valor\), ''\)/); assert.deepEqual(params, [136924]); return [mockedPoints]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/136924/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.equal(response.body.points[0].id_punto_ref, null); assert.equal(response.body.points[0].obs, 'texto plano legado'); }); test('GET /api/trips/:id/intermediate-points conserva fallback a observacion base cuando valor legado no trae obs', async () => { let step = 0; const mockedPoints = [ { id_punto_viaje: 50121, id_punto: 151, id_punto_ref: 151, posicion: 1, nombre: 'PUNTO BASE', contacto: 'Contacto Base', telefono: '600000123', direccion: 'Direccion Base', latitud: 40.41, longitud: -3.7, obs: 'OBSERVACION BASE', id_estado_intermedio: null, estado_intermedio: '', estado_intermedio_en: '', fecha_y_hora: null, ind_fallido: 0, latitud_estado: null, longitud_estado: null, fecha_hora: null, datetime: null } ]; db.query = async (sql, params) => { step += 1; if (step === 1) { return [[{ id_viaje: 136924 }]]; } if (step === 2) { return [[{ authorized: 1 }]]; } assert.match(sql, /COALESCE\(\s*vp\.obs,\s*COALESCE\(m\.observaciones, ''\)/); assert.match(sql, /REGEXP '\^\[0-9\]\+\$'/); assert.deepEqual(params, [136924]); return [mockedPoints]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/136924/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.equal(response.body.points[0].id_punto_ref, 151); assert.equal(response.body.points[0].obs, 'OBSERVACION BASE'); }); test('GET /api/trips/:id/intermediate-points devuelve [] cuando no hay puntos', async () => { let step = 0; db.query = async () => { step += 1; if (step === 1) { return [[{ id_viaje: 248230 }]]; } if (step === 2) { return [[{ authorized: 1 }]]; } return [[]]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/248230/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 200); assert.deepEqual(response.body, { success: true, trip_id: 248230, points: [] }); }); test('GET /api/trips/:id/intermediate-points 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/136924/intermediate-points' }) ); assert.equal(response.statusCode, 401); assert.deepEqual(response.body, { error: 'Unauthorized' }); }); test('GET /api/trips/:id/intermediate-points devuelve 403 para viaje no autorizado', async () => { let step = 0; db.query = async () => { step += 1; if (step === 1) { return [[{ id_viaje: 263483 }]]; } if (step === 2) { return [[]]; } throw new Error('Points query should not run when trip is forbidden'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/263483/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 403); assert.deepEqual(response.body, { success: false, error: 'Forbidden' }); }); test('GET /api/trips/:id/intermediate-points devuelve 404 cuando viaje no existe', async () => { db.query = async () => [[]]; const response = await withServer(async (server) => requestJson({ port: server.address().port, path: '/api/trips/99999999/intermediate-points', authorization: `Bearer ${createToken()}` }) ); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { success: false, error: 'Trip not found' }); });