const assert = require('node:assert/strict'); const fs = require('node:fs'); const http = require('node:http'); const path = require('node:path'); const test = require('node:test'); const jwt = require('jsonwebtoken'); const app = require('../app'); const db = require('../src/config/db'); const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', 'uploads', 'profile'); const JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret'; process.env.JWT_SECRET = JWT_SECRET; const TEST_JPEG_BUFFER = Buffer.from([ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0xff, 0xd9, 0x00, 0x00 ]); let originalQuery; 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 requestMultipart = async ({ port, path: requestPath, authorization, fields = {}, file }) => new Promise((resolve, reject) => { const boundary = `----NodeBoundary${Date.now().toString(16)}`; const chunks = []; for (const [key, value] of Object.entries(fields)) { chunks.push(Buffer.from(`--${boundary}\r\n`)); chunks.push( Buffer.from(`Content-Disposition: form-data; name="${key}"\r\n\r\n${String(value)}\r\n`) ); } if (file) { chunks.push(Buffer.from(`--${boundary}\r\n`)); chunks.push( Buffer.from( `Content-Disposition: form-data; name="${file.fieldName}"; filename="${file.filename}"\r\n` + `Content-Type: ${file.contentType}\r\n\r\n` ) ); chunks.push(file.content); chunks.push(Buffer.from('\r\n')); } chunks.push(Buffer.from(`--${boundary}--\r\n`)); const bodyBuffer = Buffer.concat(chunks); const req = http.request( { hostname: '127.0.0.1', port, method: 'POST', path: requestPath, headers: { ...(authorization ? { authorization } : {}), 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': bodyBuffer.length } }, (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(bodyBuffer); req.end(); }); const listProfileUploads = () => { if (!fs.existsSync(PROFILE_UPLOADS_DIR)) { return []; } return fs .readdirSync(PROFILE_UPLOADS_DIR, { withFileTypes: true }) .filter((entry) => entry.isFile()) .map((entry) => entry.name) .sort(); }; test.before(() => { originalQuery = db.query; fs.mkdirSync(PROFILE_UPLOADS_DIR, { recursive: true }); }); test.after(() => { db.query = originalQuery; fs.rmSync(PROFILE_UPLOADS_DIR, { recursive: true, force: true }); }); test.afterEach(() => { db.query = originalQuery; fs.rmSync(PROFILE_UPLOADS_DIR, { recursive: true, force: true }); fs.mkdirSync(PROFILE_UPLOADS_DIR, { recursive: true }); }); test('POST /update_profile_photo rechaza payload de carnet y borra temporal', async () => { db.query = async () => { throw new Error('db.query should not be called when payload is identified as driver license'); }; const beforeFiles = listProfileUploads(); const response = await withServer(async (server) => requestMultipart({ port: server.address().port, path: '/update_profile_photo', authorization: `Bearer ${createToken()}`, fields: { dni: '58045340X', id_proveedor: 675, document_type: 'driver_license', document_side: 'front' }, file: { fieldName: 'foto_perfil', filename: 'carnet-frontal.jpg', contentType: 'image/jpeg', content: TEST_JPEG_BUFFER } }) ); const afterFiles = listProfileUploads(); assert.equal(response.statusCode, 400); assert.deepEqual(response.body, { error: 'Para carnet de conducir usa /api/update_driver_license (document_type=driver_license).' }); assert.deepEqual(afterFiles, beforeFiles); }); test('POST /update_profile_photo devuelve 403 al intentar actualizar otro dni/proveedor', async () => { db.query = async () => { throw new Error('db.query should not be called for unauthorized target'); }; const beforeFiles = listProfileUploads(); const response = await withServer(async (server) => requestMultipart({ port: server.address().port, path: '/update_profile_photo', authorization: `Bearer ${createToken()}`, fields: { dni: '11111111A', id_proveedor: 9999 }, file: { fieldName: 'foto_perfil', filename: 'perfil.jpg', contentType: 'image/jpeg', content: TEST_JPEG_BUFFER } }) ); const afterFiles = listProfileUploads(); assert.equal(response.statusCode, 403); assert.deepEqual(response.body, { error: 'Forbidden' }); assert.deepEqual(afterFiles, beforeFiles); });