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 tripIncidenceMailer = require('../src/services/tripIncidenceMailer'); const JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret'; process.env.JWT_SECRET = JWT_SECRET; let originalQuery; let originalSendTripIncidenceEmail; 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: requestPath, authorization, body }) => new Promise((resolve, reject) => { const rawBody = body ? JSON.stringify(body) : ''; const req = http.request( { hostname: '127.0.0.1', port, method, path: requestPath, 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; originalSendTripIncidenceEmail = tripIncidenceMailer.sendTripIncidenceEmail; }); test.after(() => { db.query = originalQuery; tripIncidenceMailer.sendTripIncidenceEmail = originalSendTripIncidenceEmail; }); test('POST /api/trips/:tripId/incidencias 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/:tripId/incidencias' && layer.route.methods.post ); assert.ok(routeLayer, 'POST /api/trips/:tripId/incidencias route is not defined'); }); test('POST /api/trips/:tripId/incidencias 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/248230/incidencias', body: { incidencia: 'Retraso por tráfico' } }) ); assert.equal(response.statusCode, 401); assert.deepEqual(response.body, { error: 'Unauthorized' }); }); test('POST /api/trips/:tripId/incidencias valida payload inválido => 400', async () => { db.query = async () => { throw new Error('db.query should not be called for invalid payload'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/not-a-number/incidencias', authorization: `Bearer ${createToken()}`, body: { incidencia: ' ' } }) ); assert.equal(response.statusCode, 400); assert.deepEqual(response.body, { success: false, error: 'Invalid payload' }); }); test('POST /api/trips/:tripId/incidencias devuelve 404 si viaje no existe', async () => { db.query = async (sql, params) => { assert.match(sql, /FROM c_viajes/); assert.deepEqual(params, [248230]); return [[]]; }; tripIncidenceMailer.sendTripIncidenceEmail = async () => { throw new Error('mailer should not run when trip does not exist'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/incidencias', authorization: `Bearer ${createToken()}`, body: { incidencia: 'Incidencia de prueba' } }) ); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { success: false, error: 'Trip not found' }); }); test('POST /api/trips/:tripId/incidencias devuelve 403 si no autorizado', async () => { let step = 0; db.query = async (_sql, _params) => { step += 1; if (step === 1) { return [[{ id_viaje: 248230 }]]; } return [[]]; }; tripIncidenceMailer.sendTripIncidenceEmail = async () => { throw new Error('mailer should not run on forbidden'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/incidencias', authorization: `Bearer ${createToken()}`, body: { incidencia: 'Incidencia de prueba' } }) ); assert.equal(response.statusCode, 403); assert.deepEqual(response.body, { success: false, error: 'Forbidden' }); assert.equal(step, 2); }); test('POST /api/trips/:tripId/incidencias crea incidencia con avisos forzados y devuelve 201', async () => { const sentPayloads = []; let step = 0; db.query = async (sql, params) => { step += 1; if (step === 1) { assert.match(sql, /FROM c_viajes/); assert.deepEqual(params, [248230]); return [[{ id_viaje: 248230 }]]; } if (step === 2) { assert.match(sql, /FROM c_viajes_proveedor/); assert.deepEqual(params, [248230, '58045340X']); return [[{ n_proveedor: 1 }]]; } assert.match(sql, /INSERT INTO c_viajes_incidencias/); assert.equal(params[0], 248230); assert.equal(params[1], 'Retraso por atasco'); assert.equal(params[2], null); assert.equal(params[3], '58045340X'); assert.equal(params[4], 1); assert.equal(params[5], 1); return [{ affectedRows: 1 }]; }; tripIncidenceMailer.sendTripIncidenceEmail = async (payload) => { sentPayloads.push(payload); return { status: 'sent', recipientsCount: 3 }; }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/incidencias', authorization: `Bearer ${createToken({ id: '1' })}`, body: { incidencia: ' Retraso por atasco\u0000 ', notificar: 0, notificar_cr: 0 } }) ); assert.equal(response.statusCode, 201); assert.deepEqual(response.body, { success: true, message: 'correcto' }); assert.deepEqual(sentPayloads, [ { tripId: 248230, incidencia: 'Retraso por atasco', userId: 1 } ]); assert.equal(step, 3); }); test('POST /api/trips/:tripId/incidencias responde 201 con warning si falla email', async () => { let step = 0; db.query = async () => { step += 1; if (step === 1) { return [[{ id_viaje: 248230 }]]; } if (step === 2) { return [[{ n_proveedor: 1 }]]; } return [{ affectedRows: 1 }]; }; tripIncidenceMailer.sendTripIncidenceEmail = async () => { throw new Error('forced SMTP failure'); }; const response = await withServer(async (server) => requestJson({ port: server.address().port, method: 'POST', path: '/api/trips/248230/incidencias', authorization: `Bearer ${createToken()}`, body: { incidencia: 'Incidencia de prueba' } }) ); assert.equal(response.statusCode, 201); assert.deepEqual(response.body, { success: true, message: 'correcto', warning: 'email_failed' }); assert.equal(step, 3); });