Traslada los cambios de 7fbfd300b4ea17212c04528145d366a3f00b91e5 conservando la configuracion de entorno de produccion.
255 lines
12 KiB
JavaScript
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; }
|
|
});
|