Files
ABIANAPP_NODE_PRODUCCION/test/trips.ecmr.integration.test.js
roganet dc545c9d86 feat: incorporar eCMR y mercancias desde 7fbfd30
Traslada los cambios de 7fbfd300b4ea17212c04528145d366a3f00b91e5 conservando la configuracion de entorno de produccion.
2026-09-29 13:32:12 +02:00

255 lines
12 KiB
JavaScript

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; }
});