chore: sincronizar cambios pendientes de desarrollo a produccion
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# Manual de despliegue a produccion: paginacion de GET /api/trips
|
||||
|
||||
## Alcance
|
||||
|
||||
Este despliegue publica el cambio del backend para que `GET /api/trips` aplique paginacion y filtros en SQL.
|
||||
|
||||
Incluye:
|
||||
|
||||
- Cambios en `src/controllers/tripsController.js`.
|
||||
- Tests de integracion en `test/trips.list.integration.test.js`.
|
||||
- Documentacion en `README.md`.
|
||||
- Migracion de indices: `src/sql/migrations/20260630_trips_list_pagination_indexes.sql`.
|
||||
|
||||
No incluye cambios en la app React Native.
|
||||
|
||||
## Supuestos
|
||||
|
||||
- El servidor Node de produccion usa este proyecto: `/var/www/node.gestion.abianservice.com`.
|
||||
- La API escucha en `127.0.0.1:3001` salvo que se sobreescriba `PORT`.
|
||||
- El restart operativo se hace con `src/scripts/restartNodeApi.sh`.
|
||||
- Las variables `DB_HOST`, `DB_USER`, `DB_PASSWORD` y `DB_NAME` de produccion estan configuradas en `.env` o disponibles en shell.
|
||||
- Las migraciones SQL se aplican manualmente con `mysql`.
|
||||
|
||||
Si alguno de estos puntos no coincide con produccion, parar y ajustar el manual antes de ejecutar.
|
||||
|
||||
## Criterios de exito
|
||||
|
||||
- `GET /api/trips` sin parametros responde `200` con maximo 25 viajes por defecto.
|
||||
- La respuesta incluye `trips`, `page`, `limit`, `total` y `has_more`.
|
||||
- Las llamadas con `page`, `limit`, `status_ids`, `date_from` y `date_to` filtran en base de datos.
|
||||
- La API arranca correctamente y `/health` responde `200`.
|
||||
- `/api/trips/active` sin token responde `401`.
|
||||
- No aparecen errores nuevos en el log de la API.
|
||||
|
||||
## 1. Precheck antes de tocar produccion
|
||||
|
||||
Desde la raiz del proyecto:
|
||||
|
||||
```bash
|
||||
cd /var/www/node.gestion.abianservice.com
|
||||
|
||||
git status --short
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
Revisar que no haya cambios locales no esperados. Si hay cambios ajenos, no sobrescribirlos.
|
||||
|
||||
Ejecutar tests antes de desplegar:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Tambien se puede ejecutar solo la cobertura del endpoint:
|
||||
|
||||
```bash
|
||||
node --test test/trips.list.integration.test.js
|
||||
```
|
||||
|
||||
## 2. Backup rapido de base de datos
|
||||
|
||||
Antes de aplicar indices, sacar al menos backup de las tablas tocadas por la migracion:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/abian-db-backups
|
||||
|
||||
mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" \
|
||||
c_viajes c_viajes_proveedor c_cambios_estado \
|
||||
> /tmp/abian-db-backups/trips_indexes_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
Si `DB_PASSWORD` no esta exportada en shell, cargar `.env` de forma controlada o introducir la password manualmente con `-p`.
|
||||
|
||||
## 3. Aplicar codigo
|
||||
|
||||
Actualizar el codigo de produccion con el mecanismo habitual del equipo. Si se usa Git en el servidor:
|
||||
|
||||
```bash
|
||||
cd /var/www/node.gestion.abianservice.com
|
||||
|
||||
git fetch --all --prune
|
||||
git status --short
|
||||
```
|
||||
|
||||
Antes de cambiar de revision, confirmar que `git status --short` no muestra cambios locales que no deban pisarse.
|
||||
|
||||
Despues de actualizar la rama o revision objetivo, instalar dependencias solo si `package-lock.json` cambio:
|
||||
|
||||
```bash
|
||||
npm ci --omit=dev
|
||||
```
|
||||
|
||||
Para este cambio no deberia ser necesario si solo se despliega la paginacion de viajes.
|
||||
|
||||
## 4. Aplicar migracion de indices
|
||||
|
||||
La migracion es idempotente: comprueba `information_schema.statistics` antes de crear cada indice.
|
||||
|
||||
```bash
|
||||
cd /var/www/node.gestion.abianservice.com
|
||||
|
||||
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" \
|
||||
< src/sql/migrations/20260630_trips_list_pagination_indexes.sql
|
||||
```
|
||||
|
||||
Validar que los indices existen:
|
||||
|
||||
```bash
|
||||
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "
|
||||
SHOW INDEX FROM c_viajes WHERE Key_name = 'idx_c_viajes_id_estado';
|
||||
SHOW INDEX FROM c_viajes_proveedor WHERE Key_name IN (
|
||||
'idx_c_viajes_proveedor_fecha_salida',
|
||||
'idx_c_viajes_proveedor_fecha_llegada',
|
||||
'idx_c_viajes_proveedor_dni_estado_dates'
|
||||
);
|
||||
SHOW INDEX FROM c_cambios_estado WHERE Key_name = 'idx_c_cambios_estado_viaje_estado_fecha';
|
||||
"
|
||||
```
|
||||
|
||||
Si la tabla es grande, esta fase puede tardar. No reiniciar la API hasta que termine correctamente.
|
||||
|
||||
## 5. Reiniciar API
|
||||
|
||||
Usar el script incluido en el repo:
|
||||
|
||||
```bash
|
||||
cd /var/www/node.gestion.abianservice.com
|
||||
|
||||
bash src/scripts/restartNodeApi.sh
|
||||
```
|
||||
|
||||
El script valida:
|
||||
|
||||
- Proceso Node arrancado.
|
||||
- Puerto `127.0.0.1:3001` escuchando.
|
||||
- `/health` responde `200`.
|
||||
- `/api/trips/active` sin token responde `401`.
|
||||
|
||||
Si produccion usa otro puerto o ruta, exportar variables antes de ejecutar:
|
||||
|
||||
```bash
|
||||
PORT=3001 HOST=127.0.0.1 bash src/scripts/restartNodeApi.sh
|
||||
```
|
||||
|
||||
## 6. Smoke tests post-deploy
|
||||
|
||||
### Sin token
|
||||
|
||||
Debe devolver `401`:
|
||||
|
||||
```bash
|
||||
curl -i "http://127.0.0.1:3001/api/trips"
|
||||
```
|
||||
|
||||
### Con token real de conductor
|
||||
|
||||
Usar un JWT valido de la app o de una sesion de pruebas.
|
||||
|
||||
```bash
|
||||
TOKEN="<JWT_VALIDO>"
|
||||
|
||||
curl -sS "http://127.0.0.1:3001/api/trips?page=1&limit=25" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Comprobar que la respuesta contiene:
|
||||
|
||||
```json
|
||||
{
|
||||
"trips": [],
|
||||
"page": 1,
|
||||
"limit": 25,
|
||||
"total": 0,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
`trips` puede traer datos reales, pero `page`, `limit`, `total` y `has_more` deben existir.
|
||||
|
||||
Probar historico:
|
||||
|
||||
```bash
|
||||
curl -sS "http://127.0.0.1:3001/api/trips?page=1&limit=25&status_ids=7,8,9" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Probar proximos desde una fecha:
|
||||
|
||||
```bash
|
||||
curl -sS "http://127.0.0.1:3001/api/trips?page=1&limit=25&date_from=2026-06-30" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Probar rango de fechas:
|
||||
|
||||
```bash
|
||||
curl -sS "http://127.0.0.1:3001/api/trips?page=1&limit=25&date_from=2026-06-01&date_to=2026-06-30" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Validaciones negativas
|
||||
|
||||
Deben devolver `400`:
|
||||
|
||||
```bash
|
||||
curl -i "http://127.0.0.1:3001/api/trips?page=0" -H "Authorization: Bearer $TOKEN"
|
||||
curl -i "http://127.0.0.1:3001/api/trips?limit=101" -H "Authorization: Bearer $TOKEN"
|
||||
curl -i "http://127.0.0.1:3001/api/trips?status_ids=7,x" -H "Authorization: Bearer $TOKEN"
|
||||
curl -i "http://127.0.0.1:3001/api/trips?date_from=2026-02-30" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 7. Logs a revisar
|
||||
|
||||
Log por defecto del script de restart:
|
||||
|
||||
```bash
|
||||
tail -n 100 /tmp/node-gestion-api.log
|
||||
```
|
||||
|
||||
Buscar entradas del listado:
|
||||
|
||||
```bash
|
||||
grep "\[TripsList\]" /tmp/node-gestion-api.log | tail -n 20
|
||||
```
|
||||
|
||||
Cada entrada debe mostrar `page`, `limit`, `rows`, `total` y `elapsed_ms`.
|
||||
|
||||
Senales de problema:
|
||||
|
||||
- `Error getting trips list`.
|
||||
- `ER_PARSE_ERROR`.
|
||||
- `ER_BAD_FIELD_ERROR`.
|
||||
- Tiempos `elapsed_ms` altos de forma sostenida.
|
||||
- Respuestas sin `page`, `limit`, `total` o `has_more`.
|
||||
|
||||
## 8. Rollback
|
||||
|
||||
### Rollback de codigo
|
||||
|
||||
Volver a la revision anterior con el mecanismo habitual del equipo y reiniciar:
|
||||
|
||||
```bash
|
||||
cd /var/www/node.gestion.abianservice.com
|
||||
|
||||
bash src/scripts/restartNodeApi.sh
|
||||
```
|
||||
|
||||
### Rollback de indices
|
||||
|
||||
Normalmente no hace falta retirar indices: son compatibles y no cambian datos. Solo retirarlos si se confirma impacto operativo.
|
||||
|
||||
Comandos de retirada, si fueran necesarios:
|
||||
|
||||
```sql
|
||||
ALTER TABLE c_viajes DROP INDEX idx_c_viajes_id_estado;
|
||||
ALTER TABLE c_viajes_proveedor DROP INDEX idx_c_viajes_proveedor_fecha_salida;
|
||||
ALTER TABLE c_viajes_proveedor DROP INDEX idx_c_viajes_proveedor_fecha_llegada;
|
||||
ALTER TABLE c_viajes_proveedor DROP INDEX idx_c_viajes_proveedor_dni_estado_dates;
|
||||
ALTER TABLE c_cambios_estado DROP INDEX idx_c_cambios_estado_viaje_estado_fecha;
|
||||
```
|
||||
|
||||
Ejecutarlos solo tras confirmar que esos indices no existian antes del despliegue.
|
||||
|
||||
## 9. Checklist final
|
||||
|
||||
- [ ] Tests ejecutados antes del despliegue.
|
||||
- [ ] Backup de tablas creado.
|
||||
- [ ] Codigo actualizado.
|
||||
- [ ] Migracion de indices aplicada sin errores.
|
||||
- [ ] API reiniciada con `restartNodeApi.sh`.
|
||||
- [ ] `/health` responde `200`.
|
||||
- [ ] `/api/trips/active` sin token responde `401`.
|
||||
- [ ] `GET /api/trips?page=1&limit=25` con token responde metadata de paginacion.
|
||||
- [ ] Filtros `status_ids`, `date_from` y `date_to` verificados.
|
||||
- [ ] Logs sin errores nuevos.
|
||||
@@ -77,6 +77,15 @@ También puedes usar el script de npm:
|
||||
npm run notify:test -- --token "<FCM_DEVICE_TOKEN>" --dry-run
|
||||
```
|
||||
|
||||
## API: Listado de viajes
|
||||
|
||||
Endpoint: `GET /api/trips`
|
||||
Auth: `Bearer <JWT>` obligatorio.
|
||||
|
||||
Parametros opcionales: `page` (default `1`), `limit` (default `25`, maximo `100`), `status_ids` separado por comas, `date_from` y `date_to` en formato `YYYY-MM-DD`.
|
||||
|
||||
La respuesta mantiene `trips` con los campos legacy de cada viaje y añade `page`, `limit`, `total` y `has_more`. La paginacion y filtros se aplican en SQL. El orden estable del listado es `COALESCE(p.fecha_salida, v.fecha_salida) DESC, p.id_viaje DESC`.
|
||||
|
||||
## Fotos de estado de viaje (almacenamiento temporal dual)
|
||||
|
||||
Para `POST /api/trips/:id/status`, las fotos pueden guardarse en local y replicarse por SFTP en paralelo.
|
||||
@@ -103,6 +112,20 @@ TRIP_STATUS_SFTP_PASSWORD=********
|
||||
TRIP_STATUS_SFTP_REMOTE_BASE_DIR=/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native
|
||||
```
|
||||
|
||||
## Diagnostico de subidas desde la app
|
||||
|
||||
Las peticiones `POST` multipart dejan eventos JSON en `POSTS_LOG_PATH` (por defecto,
|
||||
`/var/log/posts.log`):
|
||||
|
||||
- `upload_request_started`: la API recibio la peticion.
|
||||
- `upload_request_finished`: incluye estado HTTP, duracion, fase del posible fallo y
|
||||
resultado del parser multipart.
|
||||
- `upload_request_interrupted`: el cliente o la conexion se cerro antes de terminar.
|
||||
|
||||
Todos los eventos de una peticion comparten `request_id`, que tambien se devuelve en
|
||||
la cabecera `x-request-id`. No se registran el token, los valores del formulario, el
|
||||
contenido del archivo ni su nombre original.
|
||||
|
||||
## API: Crear incidencia de viaje
|
||||
|
||||
Endpoint: `POST /api/trips/:tripId/incidencias`
|
||||
|
||||
@@ -8,7 +8,7 @@ NODE_BIN="${NODE_BIN:-$(command -v node || true)}"
|
||||
HOST="${HOST:-127.0.0.1}"
|
||||
PORT="${PORT:-3001}"
|
||||
LOG_FILE="${LOG_FILE:-/tmp/node-gestion-api.log}"
|
||||
HEALTH_PATH="${HEALTH_PATH:-/}"
|
||||
HEALTH_PATH="${HEALTH_PATH:-/health}"
|
||||
AUTH_CHECK_PATH="${AUTH_CHECK_PATH:-/api/trips/active}"
|
||||
AUTH_EXPECTED_STATUS="${AUTH_EXPECTED_STATUS:-401}"
|
||||
STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-30}"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
SET @sql := IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'c_viajes'
|
||||
AND index_name = 'idx_c_viajes_id_estado') = 0,
|
||||
'ALTER TABLE c_viajes ADD KEY idx_c_viajes_id_estado (id_estado)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'c_viajes_proveedor'
|
||||
AND index_name = 'idx_c_viajes_proveedor_fecha_salida') = 0,
|
||||
'ALTER TABLE c_viajes_proveedor ADD KEY idx_c_viajes_proveedor_fecha_salida (fecha_salida)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'c_viajes_proveedor'
|
||||
AND index_name = 'idx_c_viajes_proveedor_fecha_llegada') = 0,
|
||||
'ALTER TABLE c_viajes_proveedor ADD KEY idx_c_viajes_proveedor_fecha_llegada (fecha_llegada)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'c_viajes_proveedor'
|
||||
AND index_name = 'idx_c_viajes_proveedor_dni_estado_dates') = 0,
|
||||
'ALTER TABLE c_viajes_proveedor ADD KEY idx_c_viajes_proveedor_dni_estado_dates (dni, id_viaje, fecha_salida, fecha_llegada)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
(SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'c_cambios_estado'
|
||||
AND index_name = 'idx_c_cambios_estado_viaje_estado_fecha') = 0,
|
||||
'ALTER TABLE c_cambios_estado ADD KEY idx_c_cambios_estado_viaje_estado_fecha (id_viaje, id_estado, fecha_y_hora)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -108,6 +108,7 @@ test('GET /api/trips/active devuelve viaje activo', async () => {
|
||||
cod_viaje: 'AB-2024-001',
|
||||
n_proveedor: 1,
|
||||
id_estado: 4,
|
||||
statusId: 4,
|
||||
estado: 'CARGA DE MERCANCÍA',
|
||||
estado_en: 'CARGO LOADING',
|
||||
nombrea: 'ORIGEN',
|
||||
@@ -122,13 +123,19 @@ test('GET /api/trips/active devuelve viaje activo', async () => {
|
||||
};
|
||||
|
||||
db.query = async (sql, params) => {
|
||||
assert.match(sql, /FROM c_viajes_proveedor p/);
|
||||
if (/FROM c_viajes_proveedor p/.test(sql)) {
|
||||
assert.match(sql, /INNER JOIN t_viaje_estados e/);
|
||||
assert.match(sql, /fecha_salida/);
|
||||
assert.match(sql, /fecha_llegada/);
|
||||
assert.match(sql, /INNER JOIN m_proveedores_trasportistas t/);
|
||||
assert.deepEqual(params, ['58045340X', 2, 6]);
|
||||
assert.deepEqual(params, ['58045340X', 2, 6, '58045340X', 1]);
|
||||
return [[mockedTrip]];
|
||||
}
|
||||
|
||||
assert.match(sql, /FROM c_cambios_estado c/);
|
||||
assert.match(sql, /FROM c_viajes_puntos vp/);
|
||||
assert.deepEqual(params, [123, 123, 3, 4, 5]);
|
||||
return [[]];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
|
||||
@@ -540,9 +540,17 @@ test('POST /api/trips/:id/status multipart registra body parseado y ficheros en
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
await waitFor(() => fs.existsSync(TEST_POSTS_LOG_PATH));
|
||||
await waitFor(() =>
|
||||
fs.existsSync(TEST_POSTS_LOG_PATH) &&
|
||||
readJsonLines(TEST_POSTS_LOG_PATH).some(
|
||||
(logEntry) => logEntry.body?.id_estado === '5'
|
||||
)
|
||||
);
|
||||
|
||||
const [entry] = readJsonLines(TEST_POSTS_LOG_PATH);
|
||||
const entry = readJsonLines(TEST_POSTS_LOG_PATH).find(
|
||||
(logEntry) => logEntry.body?.id_estado === '5'
|
||||
);
|
||||
assert.ok(entry, 'Expected parsed multipart payload log entry');
|
||||
assert.equal(entry.method, 'POST');
|
||||
assert.equal(entry.path, '/api/trips/248230/status');
|
||||
assert.equal(entry.body.id_estado, '5');
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user