const assert = require('node:assert/strict'); const fs = require('node:fs'); const http = require('node:http'); const path = require('node:path'); 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; const TEST_STATUS_LOG_PATH = path.resolve(__dirname, '..', 'tmp', 'test-intermediate-point-status.log'); 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, method, path, authorization, body }) => new Promise((resolve, reject) => { const rawBody = body ? JSON.stringify(body) : ''; const req = http.request( { hostname: '127.0.0.1', port, method, path, headers: { ...(authorization ? { authorization } : {}), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(rawBody) } }, (res) => { let responseBody = ''; res.on('data', (chunk) => { responseBody += chunk; }); res.on('end', () => { resolve({ statusCode: res.statusCode, body: responseBody ? JSON.parse(responseBody) : null }); }); } ); req.on('error', reject); req.write(rawBody); req.end(); }); const waitFor = async (predicate, { timeoutMs = 1500, intervalMs = 25 } = {}) => { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { if (predicate()) { return; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error('Timed out waiting for condition'); }; const readJsonLines = (filePath) => fs .readFileSync(filePath, 'utf8') .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => JSON.parse(line)); test.before(() => { originalQuery = db.query; fs.rmSync(TEST_STATUS_LOG_PATH, { force: true }); }); test.after(() => { db.query = originalQuery; delete process.env.TRIP_STATUS_UPDATES_LOG_PATH; fs.rmSync(TEST_STATUS_LOG_PATH, { force: true }); }); test.afterEach(() => { db.query = originalQuery; delete process.env.TRIP_STATUS_UPDATES_LOG_PATH; fs.rmSync(TEST_STATUS_LOG_PATH, { force: true }); }); test('POST /api/trips/:id/intermediate-points/:pointId/status 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 pointStatusRouteLayer = apiRouterLayers .flatMap((routerLayer) => routerLayer.handle.stack) .find( (layer) => layer.route && layer.route.path === '/trips/:id/intermediate-points/:pointId/status' && layer.route.methods.post ); assert.ok( pointStatusRouteLayer, 'POST /api/trips/:id/intermediate-points/:pointId/status route is not defined' ); }); test('POST /api/trips/:id/intermediate-points/:pointId/status actualiza estado intermedio', async () => { let step = 0; 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 }]]; } if (step === 3) { assert.match(sql, /FROM c_viajes_puntos/); assert.deepEqual(params, [50101, 136924]); return [[{ id_punto: 50101 }]]; } assert.match(sql, /UPDATE c_viajes_puntos/); assert.deepEqual(params, [3, '2026-02-16 12:34:56', 0, '40.416775', '-3.70379', 50101, 136924]); return [{ affectedRows: 1 }]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/136924/intermediate-points/50101/status', authorization: `Bearer ${createToken()}`, body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40,416775', longitud: '-3,70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 200); assert.deepEqual(response.body, { success: true, trip_id: 136924, id_punto: 50101, id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 }); }); test('POST /api/trips/:id/intermediate-points/:pointId/status registra auditoria en status.log', async () => { let step = 0; process.env.TRIP_STATUS_UPDATES_LOG_PATH = TEST_STATUS_LOG_PATH; db.query = async () => { step += 1; if (step === 1) { return [[{ id_viaje: 136924 }]]; } if (step === 2) { return [[{ authorized: 1 }]]; } if (step === 3) { return [[{ id_punto: 50101 }]]; } return [{ affectedRows: 1 }]; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/136924/intermediate-points/50101/status', authorization: `Bearer ${createToken()}`, body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 200); await waitFor(() => fs.existsSync(TEST_STATUS_LOG_PATH)); const [entry] = readJsonLines(TEST_STATUS_LOG_PATH); assert.equal(entry.flow, 'intermediate_point_endpoint'); assert.equal(entry.operation, 'update_point_status'); assert.equal(entry.result, 'SUCCESS'); assert.equal(entry.trip_id, 136924); assert.equal(entry.id_punto, 50101); assert.equal(entry.id_estado, 3); assert.equal(entry.new_intermediate_status_id, 3); assert.equal(entry.fecha_y_hora, '2026-02-16 12:34:56'); assert.equal(entry.latitud, '40.416775'); assert.equal(entry.longitud, '-3.70379'); assert.equal(entry.ind_fallido, 0); }); test('POST /api/trips/:id/intermediate-points/:pointId/status devuelve 400 para payload inválido', async () => { db.query = async () => { throw new Error('db.query should not run for invalid payload'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/136924/intermediate-points/50101/status', authorization: `Bearer ${createToken()}`, body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 400); assert.deepEqual(response.body, { success: false, error: 'Invalid payload' }); }); test('POST /api/trips/:id/intermediate-points/:pointId/status 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, method: 'POST', path: '/api/trips/136924/intermediate-points/50101/status', body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 401); assert.deepEqual(response.body, { error: 'Unauthorized' }); }); test('POST /api/trips/:id/intermediate-points/:pointId/status devuelve 403 para viaje no autorizado', async () => { let step = 0; db.query = async () => { step += 1; if (step === 1) { return [[{ id_viaje: 136924 }]]; } if (step === 2) { return [[]]; } throw new Error('Point query should not run for forbidden trip'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/136924/intermediate-points/50101/status', authorization: `Bearer ${createToken()}`, body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 403); assert.deepEqual(response.body, { success: false, error: 'Forbidden' }); }); test('POST /api/trips/:id/intermediate-points/:pointId/status devuelve 404 cuando viaje no existe', async () => { db.query = async () => [[]]; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/99999999/intermediate-points/50101/status', authorization: `Bearer ${createToken()}`, body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { success: false, error: 'Trip not found' }); }); test('POST /api/trips/:id/intermediate-points/:pointId/status devuelve 404 cuando punto no existe', async () => { let step = 0; db.query = async () => { step += 1; if (step === 1) { return [[{ id_viaje: 136924 }]]; } if (step === 2) { return [[{ authorized: 1 }]]; } if (step === 3) { return [[]]; } throw new Error('Update should not run when point does not exist'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/136924/intermediate-points/99999/status', authorization: `Bearer ${createToken()}`, body: { id_estado_intermedio: 3, fecha_y_hora: '2026-02-16 12:34:56', latitud: '40.416775', longitud: '-3.70379', ind_fallido: 0 } }) ); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { success: false, error: 'Intermediate point not found' }); });