const assert = require('node:assert/strict'); const http = require('node:http'); const crypto = require('node:crypto'); const test = require('node:test'); const jwt = require('jsonwebtoken'); const express = require('express'); const db = require('../src/config/db'); const routes = require('../src/routes/tripsRoutes'); process.env.JWT_SECRET = 'ecmr-test-secret'; const originalQuery = db.query; const originalGetConnection = db.getConnection; let transaction; let server; let port; test.before(async () => { const app = express(); app.use('/api', routes); server = http.createServer(app); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); port = server.address().port; }); test.beforeEach(() => { transaction = { committed: false, rolledBack: false, released: false }; db.getConnection = async () => ({ beginTransaction: async () => {}, query: (...args) => db.query(...args), commit: async () => { transaction.committed = true; }, rollback: async () => { transaction.rolledBack = true; }, release: () => { transaction.released = true; } }); }); test.after(async () => { db.getConnection = originalGetConnection; db.query = originalQuery; await new Promise(resolve => server.close(resolve)); }); test.afterEach(() => { db.query = originalQuery; }); const request = async (id = '123', payload = { dni: '12345678A' }) => { const headers = payload === null ? {} : { authorization: `Bearer ${jwt.sign(payload, process.env.JWT_SECRET)}` }; const response = await fetch(`http://127.0.0.1:${port}/api/trips/${id}/ecmr`, { headers }); return { status: response.status, body: await response.json(), cache: response.headers.get('cache-control') }; }; for (const payload of [null, { id: 1 }]) { test(`rejects missing authentication or driver: ${JSON.stringify(payload)}`, async () => { db.query = async () => { assert.fail('Must not query database'); }; assert.equal((await request('123', payload)).status, 401); }); } for (const id of ['0', '-1', '123abc', '1.5', '9007199254740992']) { test(`rejects invalid trip id ${id}`, async () => { db.query = async () => { assert.fail('Must not query database'); }; assert.equal((await request(id)).status, 404); }); } test('returns 404 for unknown trip', async () => { db.query = async () => [[]]; assert.equal((await request()).status, 404); }); test('does not retrieve documents for another driver', async () => { let calls = 0; db.query = async (sql, params) => { calls++; if (calls === 1) return [[{ id_viaje: 123 }]]; assert.equal(calls, 2); assert.match(sql, /dni = \?/); assert.deepEqual(params, [123, '12345678A']); return [[]]; }; assert.equal((await request()).status, 403); assert.equal(calls, 2); }); for (const [loadSigned, unloadSigned] of [[0, 0], [1, 0], [0, 1], [1, 1]]) { test(`returns links only for unsigned roles (${loadSigned}, ${unloadSigned})`, async () => { const inserted = []; let calls = 0; db.query = async (sql, params) => { calls++; if (calls === 1) return [[{ id_viaje: 123 }]]; if (calls === 2) return [[{ authorized: 1 }]]; if (calls === 3) { assert.deepEqual(params, [123, '12345678A']); assert.match(sql, /p\.n_proveedor = d\.n_proveedor/); assert.match(sql, /p\.dni = \?/); assert.match(sql, /d\.activo = 1/); assert.match(sql, /d\.pdf_ruta IS NOT NULL/); assert.match(sql, /FOR UPDATE/); assert.match(sql, /ecmr_firmas/); return [[{ id_documento: 7, document_url: 'https://example.test/ecmr/documento.php?t=pdf-token', firma_carga_completada: loadSigned, firma_descarga_completada: unloadSigned }]]; } assert.match(sql, /INSERT INTO ecmr_invitaciones/); assert.match(sql, /'app'/); assert.match(sql, /NOW\(\)/); assert.equal(params[0], 7); assert.equal(params[2], '12345678A'); assert.match(params[3], /^[a-f0-9]{64}$/); assert.match(params[4], /^[a-f0-9]{64}$/); inserted.push(params); return [{ insertId: 100 + calls }]; }; const response = await request(); assert.equal(response.status, 200); assert.equal(response.cache, 'no-store'); const document = response.body.data[0]; assert.equal(document.firma_carga_completada, Boolean(loadSigned)); assert.equal(document.firma_descarga_completada, Boolean(unloadSigned)); assert.equal(document.firma_expira_en_segundos, 900); const tokens = []; for (const [role, field, signed] of [ ['toma_carga', 'firma_carga_url', loadSigned], ['entrega', 'firma_descarga_url', unloadSigned] ]) { if (signed) { assert.equal(document[field], null); continue; } const url = new URL(document[field]); assert.equal(url.origin + url.pathname, 'https://example.test/ecmr/firmar.php'); const token = url.searchParams.get('t'); assert.match(token, /^[a-f0-9]{64}$/); tokens.push(token); const row = inserted.find(params => params[1] === role); assert.equal(row[3], crypto.createHash('sha256').update(token).digest('hex')); assert.notEqual(row[4], token); } assert.equal(new Set(tokens).size, tokens.length); assert.equal(inserted.length, 2 - loadSigned - unloadSigned); assert.deepEqual(transaction, { committed: true, rolledBack: false, released: true }); }); } test('returns an empty list without creating invitations', async () => { let calls = 0; db.query = async () => { calls++; assert.ok(calls <= 3); return calls < 3 ? [[{ authorized: 1 }]] : [[]]; }; const response = await request(); assert.equal(response.status, 200); assert.deepEqual(response.body.data, []); assert.equal(transaction.committed, true); }); test('rolls back all links if an invitation fails', async () => { let calls = 0; db.query = async () => { calls++; if (calls < 3) return [[{ authorized: 1 }]]; if (calls === 3) return [[{ id_documento: 7, document_url: 'https://example.test/documento.php' }]]; if (calls === 4) return [{ insertId: 1 }]; throw new Error('forced insert failure'); }; const response = await request(); assert.equal(response.status, 500); assert.equal(response.body.success, false); assert.deepEqual(transaction, { committed: false, rolledBack: true, released: true }); }); test('rejects signing URLs without HTTPS', async () => { let calls = 0; db.query = async () => { calls++; assert.ok(calls <= 3); if (calls < 3) return [[{ authorized: 1 }]]; return [[{ id_documento: 7, document_url: 'http://example.test/documento.php' }]]; }; assert.equal((await request()).status, 500); assert.equal(transaction.rolledBack, true); }); test('database errors do not expose internal details', async () => { db.query = async () => { throw new Error('internal database detail'); }; const response = await request(); assert.equal(response.status, 500); assert.equal(response.body.error, 'Could not load eCMR documents'); }); const requestQr = async (tripId = '123', documentId = '7', payload = { dni: '12345678A' }) => { const headers = payload === null ? {} : { authorization: `Bearer ${jwt.sign(payload, process.env.JWT_SECRET)}` }; const response = await fetch(`http://127.0.0.1:${port}/api/trips/${tripId}/ecmr/${documentId}/qr`, { headers }); return { status: response.status, body: await response.json(), cache: response.headers.get('cache-control') }; }; for (const payload of [null, { id: 1 }]) { test(`QR rejects missing authentication or driver: ${JSON.stringify(payload)}`, async () => { db.query = async () => assert.fail('Must not query database'); assert.equal((await requestQr('123', '7', payload)).status, 401); }); } for (const invalid of ['0', '-1', '7abc', '1.5', '9007199254740992']) { test(`QR rejects invalid identifiers: ${invalid}`, async () => { db.query = async () => assert.fail('Must not query database'); assert.equal((await requestQr(invalid, '7')).status, 404); assert.equal((await requestQr('123', invalid)).status, 404); }); } test('QR returns only the requested active document of the assigned driver and creates no invitations', async () => { const QRCode = require('qrcode'); const originalEncode = QRCode.toDataURL; const url = 'https://example.test/ecmr/documento.php?t=' + 'a'.repeat(64); let encoded; QRCode.toDataURL = async (text, options) => { encoded = text; return originalEncode(text, options); }; try { db.getConnection = async () => assert.fail('QR must not create signing invitations'); let queries = 0; db.query = async (sql, params) => { queries++; assert.match(sql, /^SELECT/); assert.match(sql, /p\.n_proveedor = d\.n_proveedor/); assert.match(sql, /d\.id_viaje = \? AND d\.id_documento = \? AND p\.dni = \? AND d\.activo = 1/); assert.match(sql, /d\.pdf_ruta IS NOT NULL AND d\.pdf_ruta <> ''/); assert.deepEqual(params, [123, 7, '12345678A']); return [[{ id_documento: 7, n_proveedor: 2, numero: 'ECMR-123', version: 3, document_url: url }]]; }; const response = await requestQr(); assert.equal(response.status, 200); assert.equal(response.cache, 'no-store'); assert.equal(response.body.data.id_documento, 7); assert.equal(response.body.data.version, 3); assert.equal(response.body.data.document_url, url); assert.equal(encoded, url); assert.equal(queries, 1); assert.match(response.body.data.qr_data_url, /^data:image\/png;base64,/); const png = Buffer.from(response.body.data.qr_data_url.split(',')[1], 'base64'); assert.equal(png.subarray(0, 8).toString('hex'), '89504e470d0a1a0a'); assert.equal(png.readUInt32BE(16), 320); assert.equal(png.readUInt32BE(20), 320); assert.equal(response.body.data.firma_carga_url, undefined); } finally { QRCode.toDataURL = originalEncode; } }); test('QR returns 404 when no authorized active PDF matches', async () => { db.query = async () => [[]]; const response = await requestQr(); assert.equal(response.status, 404); assert.equal(response.cache, 'no-store'); assert.equal(response.body.data, undefined); }); for (const url of ['http://example.test/pdf', 'not-a-url', '']) { test(`QR rejects invalid PDF URL ${url}`, async () => { db.query = async () => [[{ document_url: url }]]; assert.equal((await requestQr()).status, 500); }); } test('QR handles database and image generation errors without leaking URLs or internals', async () => { db.query = async () => { throw new Error('private database details'); }; assert.deepEqual((await requestQr()).body, { success: false, error: 'Could not load eCMR QR' }); const QRCode = require('qrcode'), originalEncode = QRCode.toDataURL; try { db.query = async () => [[{ document_url: 'https://example.test/documento.php?t=private' }]]; QRCode.toDataURL = async () => { throw new Error('private QR details'); }; const response = await requestQr(); assert.equal(response.status, 500); assert.deepEqual(response.body, { success: false, error: 'Could not load eCMR QR' }); } finally { QRCode.toDataURL = originalEncode; } });