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 TEST_LOG_PATH = path.resolve(__dirname, '..', 'tmp', 'test-upload-diagnostics.log'); process.env.POSTS_LOG_PATH = TEST_LOG_PATH; const app = require('../app'); const JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret'; process.env.JWT_SECRET = JWT_SECRET; const createToken = () => jwt.sign( { id: 1, dni: '58045340X', id_proveedor: 675 }, 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, authorization, fields = {}, file }) => new Promise((resolve, reject) => { const boundary = `----UploadDiagnostics${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 request = http.request( { hostname: '127.0.0.1', port, method: 'POST', path: '/update_profile_photo', headers: { ...(authorization ? { authorization } : {}), 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': bodyBuffer.length } }, (response) => { let responseBody = ''; response.on('data', (chunk) => { responseBody += chunk; }); response.on('end', () => { resolve({ statusCode: response.statusCode, headers: response.headers, body: responseBody ? JSON.parse(responseBody) : null }); }); } ); request.on('error', reject); request.end(bodyBuffer); }); const readLogEntries = () => { if (!fs.existsSync(TEST_LOG_PATH)) { return []; } return fs .readFileSync(TEST_LOG_PATH, 'utf8') .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => JSON.parse(line)); }; const waitForFinishedLog = async () => { const startedAt = Date.now(); while (Date.now() - startedAt < 1500) { const finished = readLogEntries().find( (entry) => entry.event === 'upload_request_finished' ); if (finished) { return finished; } await new Promise((resolve) => setTimeout(resolve, 25)); } throw new Error('Timed out waiting for upload diagnostics log'); }; test.beforeEach(() => { fs.rmSync(TEST_LOG_PATH, { force: true }); }); test.after(() => { delete process.env.POSTS_LOG_PATH; fs.rmSync(TEST_LOG_PATH, { force: true }); }); test('multipart sin autenticacion identifica fallo antes del parser', async () => { const response = await withServer((server) => requestMultipart({ port: server.address().port, file: { fieldName: 'foto_perfil', filename: 'profile.jpg', contentType: 'image/jpeg', content: Buffer.from('jpeg') } }) ); assert.equal(response.statusCode, 403); const finished = await waitForFinishedLog(); const started = readLogEntries().find( (entry) => entry.event === 'upload_request_started' ); assert.equal(finished.request_id, response.headers['x-request-id']); assert.equal(started.request_id, finished.request_id); assert.equal(finished.failure_stage, 'authentication_rate_limit_or_route'); assert.equal(finished.upload.parser_status, 'not_reached'); }); test('tipo de archivo invalido identifica rechazo del parser multipart', async () => { const token = createToken(); const response = await withServer((server) => requestMultipart({ port: server.address().port, authorization: `Bearer ${token}`, file: { fieldName: 'foto_perfil', filename: 'profile.txt', contentType: 'text/plain', content: Buffer.from('not-an-image') } }) ); assert.equal(response.statusCode, 400); const finished = await waitForFinishedLog(); assert.equal(finished.failure_stage, 'multipart_parser'); assert.equal(finished.upload.flow, 'profile_photo'); assert.equal(finished.upload.parser_status, 'rejected'); assert.equal(finished.upload.error.code, 'INVALID_FILE_TYPE'); assert.equal(JSON.stringify(readLogEntries()).includes(token), false); assert.equal(JSON.stringify(readLogEntries()).includes('profile.txt'), false); }); test('payload invalido tras guardar el archivo identifica fallo posterior al parser', async () => { const response = await withServer((server) => requestMultipart({ port: server.address().port, authorization: `Bearer ${createToken()}`, fields: { id_proveedor: 675 }, file: { fieldName: 'foto_perfil', filename: 'profile.jpg', contentType: 'image/jpeg', content: Buffer.from('jpeg') } }) ); assert.equal(response.statusCode, 400); const finished = await waitForFinishedLog(); assert.equal(finished.failure_stage, 'controller_or_persistence'); assert.equal(finished.upload.parser_status, 'parsed'); assert.deepEqual(finished.upload.body_fields, ['id_proveedor']); assert.equal(finished.upload.file_count, 1); assert.equal(finished.upload.files[0].original_extension, '.jpg'); assert.equal(finished.upload.files[0].local_file_created, true); });