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; let originalGetConnection; 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(); }); test.before(() => { originalQuery = db.query; originalGetConnection = db.getConnection; }); test.after(() => { db.query = originalQuery; db.getConnection = originalGetConnection; }); test.afterEach(() => { db.query = originalQuery; db.getConnection = originalGetConnection; }); test('POST /api/trips/:id/auto-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 routeLayer = apiRouterLayers .flatMap((routerLayer) => routerLayer.handle.stack) .find( (layer) => layer.route && layer.route.path === '/trips/:id/auto-status' && layer.route.methods.post ); assert.ok(routeLayer, 'POST /api/trips/:id/auto-status route is not defined'); }); test('POST /api/trips/:id/auto-status con id_punto inserta c_cambios_estado y actualiza punto', async () => { let txStep = 0; let beginCalled = false; let commitCalled = false; let rollbackCalled = false; let releaseCalled = false; const connection = { beginTransaction: async () => { beginCalled = true; }, query: async (sql, params) => { txStep += 1; if (/UPDATE c_viajes\s/i.test(sql)) { throw new Error('Unexpected update on c_viajes'); } if (txStep === 1) { assert.match(sql, /FROM t_viaje_estados/); assert.deepEqual(params, [5]); return [[{ id_estado: 5 }]]; } if (txStep === 2) { assert.match(sql, /FROM c_viajes/); assert.match(sql, /FOR UPDATE/); assert.deepEqual(params, [248230]); return [[{ id_viaje: 248230 }]]; } if (txStep === 3) { assert.match(sql, /FROM c_viajes_proveedor/); assert.match(sql, /FOR UPDATE/); assert.deepEqual(params, [248230, '58045340X']); return [[{ n_proveedor: 1 }]]; } if (txStep === 4) { assert.match(sql, /INSERT INTO c_cambios_estado/); assert.match(sql, /actualizado_automaticamente/); assert.equal(params[0], 248230); assert.equal(params[1], 1); assert.equal(params[2], '58045340X'); assert.equal(params[3], 5); assert.equal(params[4], 1); assert.equal(params[5], 'estado automático punto'); assert.equal(params[6], '40.416775'); assert.equal(params[7], '-3.70379'); assert.equal(params[8], '2026-02-17 12:34:56'); assert.equal(params[11], 1); return [{ affectedRows: 1, insertId: 9001 }]; } if (txStep === 5) { assert.match(sql, /FROM c_viajes_puntos/); assert.match(sql, /FOR UPDATE/); assert.deepEqual(params, [8123, 248230]); return [[{ id_punto: 8123 }]]; } if (txStep === 6) { assert.match(sql, /UPDATE c_viajes_puntos/); assert.match(sql, /actualizado_automaticamente = 1/); assert.deepEqual(params, [5, '2026-02-17 12:34:56', 1, '40.416775', '-3.70379', 8123, 248230]); return [{ affectedRows: 1 }]; } throw new Error(`Unexpected query on step ${txStep}: ${sql}`); }, commit: async () => { commitCalled = true; }, rollback: async () => { rollbackCalled = true; }, release: () => { releaseCalled = true; } }; db.getConnection = async () => connection; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken()}`, body: { id_estado: 5, id_punto: 8123, observaciones: 'estado automático punto', ind_fallido: 1, latitud: '40,416775', longitud: '-3,70379', fecha_y_hora: '2026-02-17 12:34:56' } }) ); assert.equal(response.statusCode, 200); assert.equal(response.body.success, true); assert.equal(response.body.trip_id, 248230); assert.equal(response.body.id_estado, 5); assert.equal(response.body.id_punto, 8123); assert.equal(response.body.actualizado_automaticamente, 1); assert.match(response.body.updated_at, /^\d{4}-\d{2}-\d{2}T/); assert.equal(beginCalled, true); assert.equal(commitCalled, true); assert.equal(rollbackCalled, false); assert.equal(releaseCalled, true); assert.equal(txStep, 6); }); test('POST /api/trips/:id/auto-status global inserta c_cambios_estado y no toca puntos', async () => { let txStep = 0; const connection = { beginTransaction: async () => {}, query: async (sql, params) => { txStep += 1; if (/UPDATE c_viajes\s/i.test(sql)) { throw new Error('Unexpected update on c_viajes'); } if (/c_viajes_puntos/i.test(sql)) { throw new Error('Point tables should not be touched for global status'); } if (txStep === 1) { assert.match(sql, /FROM t_viaje_estados/); assert.deepEqual(params, [7]); return [[{ id_estado: 7 }]]; } if (txStep === 2) { assert.match(sql, /FROM c_viajes/); assert.deepEqual(params, [248230]); return [[{ id_viaje: 248230 }]]; } if (txStep === 3) { assert.match(sql, /FROM c_viajes_proveedor/); assert.deepEqual(params, [248230, '58045340X']); return [[{ n_proveedor: 1 }]]; } if (txStep === 4) { assert.match(sql, /INSERT INTO c_cambios_estado/); assert.equal(params[0], 248230); assert.equal(params[3], 7); assert.equal(params[11], 1); return [{ affectedRows: 1, insertId: 9002 }]; } throw new Error(`Unexpected query on step ${txStep}: ${sql}`); }, commit: async () => {}, rollback: async () => { throw new Error('rollback should not be called for successful request'); }, release: () => {} }; db.getConnection = async () => connection; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken()}`, body: { id_estado: 7, observaciones: 'estado global automático', latitud: '40.1', longitud: '-3.7' } }) ); assert.equal(response.statusCode, 200); assert.equal(response.body.success, true); assert.equal(response.body.trip_id, 248230); assert.equal(response.body.id_estado, 7); assert.equal(response.body.actualizado_automaticamente, 1); assert.equal(Object.prototype.hasOwnProperty.call(response.body, 'id_punto'), false); assert.equal(txStep, 4); }); test('POST /api/trips/:id/auto-status con id_punto y estado no intermedio devuelve 422', async () => { db.getConnection = async () => { throw new Error('db.getConnection should not run for invalid point status'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken()}`, body: { id_estado: 7, id_punto: 8123 } }) ); assert.equal(response.statusCode, 422); assert.deepEqual(response.body, { success: false, error: 'Invalid point status' }); }); test('POST /api/trips/:id/auto-status devuelve 400 para payload inválido', async () => { db.getConnection = async () => { throw new Error('db.getConnection should not run for invalid payload'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken()}`, body: { id_punto: 8123 } }) ); assert.equal(response.statusCode, 400); assert.deepEqual(response.body, { success: false, error: 'Invalid payload' }); }); test('POST /api/trips/:id/auto-status devuelve 401 sin token', async () => { db.getConnection = async () => { throw new Error('db.getConnection should not be called without token'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', body: { id_estado: 5, id_punto: 8123 } }) ); assert.equal(response.statusCode, 401); assert.deepEqual(response.body, { error: 'Unauthorized' }); }); test('POST /api/trips/:id/auto-status devuelve 403 para viaje no autorizado', async () => { let txStep = 0; let rollbackCalled = false; const connection = { beginTransaction: async () => {}, query: async (sql) => { txStep += 1; if (txStep === 1) { assert.match(sql, /FROM t_viaje_estados/); return [[{ id_estado: 7 }]]; } if (txStep === 2) { assert.match(sql, /FROM c_viajes/); return [[{ id_viaje: 248230 }]]; } if (txStep === 3) { assert.match(sql, /FROM c_viajes_proveedor/); return [[]]; } throw new Error('No further queries should run for forbidden trip'); }, commit: async () => { throw new Error('commit should not be called'); }, rollback: async () => { rollbackCalled = true; }, release: () => {} }; db.getConnection = async () => connection; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken({ dni: '00000000T' })}`, body: { id_estado: 7 } }) ); assert.equal(response.statusCode, 403); assert.deepEqual(response.body, { success: false, error: 'Forbidden' }); assert.equal(rollbackCalled, true); assert.equal(txStep, 3); }); test('POST /api/trips/:id/auto-status devuelve 404 cuando viaje no existe', async () => { let txStep = 0; let rollbackCalled = false; const connection = { beginTransaction: async () => {}, query: async (sql) => { txStep += 1; if (txStep === 1) { assert.match(sql, /FROM t_viaje_estados/); return [[{ id_estado: 7 }]]; } if (txStep === 2) { assert.match(sql, /FROM c_viajes/); return [[]]; } throw new Error('Authorization query should not run when trip does not exist'); }, commit: async () => { throw new Error('commit should not be called'); }, rollback: async () => { rollbackCalled = true; }, release: () => {} }; db.getConnection = async () => connection; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/99999999/auto-status', authorization: `Bearer ${createToken()}`, body: { id_estado: 7 } }) ); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { success: false, error: 'Trip not found' }); assert.equal(rollbackCalled, true); assert.equal(txStep, 2); }); test('POST /api/trips/:id/auto-status devuelve 404 cuando punto no existe', async () => { let txStep = 0; let rollbackCalled = false; const connection = { beginTransaction: async () => {}, query: async (sql) => { txStep += 1; if (txStep === 1) { return [[{ id_estado: 5 }]]; } if (txStep === 2) { return [[{ id_viaje: 248230 }]]; } if (txStep === 3) { return [[{ n_proveedor: 1 }]]; } if (txStep === 4) { assert.match(sql, /INSERT INTO c_cambios_estado/); return [{ affectedRows: 1, insertId: 9003 }]; } if (txStep === 5) { assert.match(sql, /FROM c_viajes_puntos/); return [[]]; } throw new Error('Point update should not run when point does not exist'); }, commit: async () => { throw new Error('commit should not be called'); }, rollback: async () => { rollbackCalled = true; }, release: () => {} }; db.getConnection = async () => connection; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken()}`, body: { id_estado: 5, id_punto: 99999 } }) ); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { success: false, error: 'Trip point not found' }); assert.equal(rollbackCalled, true); assert.equal(txStep, 5); }); test('POST /api/trips/:id/auto-status devuelve 500 y hace rollback en error transaccional', async () => { let txStep = 0; let rollbackCalled = false; let releaseCalled = false; const connection = { beginTransaction: async () => {}, query: async (sql) => { txStep += 1; if (txStep === 1) { return [[{ id_estado: 7 }]]; } if (txStep === 2) { return [[{ id_viaje: 248230 }]]; } if (txStep === 3) { return [[{ n_proveedor: 1 }]]; } if (txStep === 4) { assert.match(sql, /INSERT INTO c_cambios_estado/); throw new Error('insert failed'); } throw new Error('Unexpected query execution'); }, commit: async () => { throw new Error('commit should not be called after error'); }, rollback: async () => { rollbackCalled = true; }, release: () => { releaseCalled = true; } }; db.getConnection = async () => connection; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/auto-status', authorization: `Bearer ${createToken()}`, body: { id_estado: 7 } }) ); assert.equal(response.statusCode, 500); assert.deepEqual(response.body, { success: false, error: 'Internal server error' }); assert.equal(rollbackCalled, true); assert.equal(releaseCalled, true); assert.equal(txStep, 4); });