2689 lines
81 KiB
JavaScript
2689 lines
81 KiB
JavaScript
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_UPLOAD_DIR = path.resolve(__dirname, '..', 'tmp', 'test-status-uploads');
|
|
const TEST_POSTS_LOG_PATH = path.resolve(__dirname, '..', 'tmp', 'test-posts.log');
|
|
const TEST_STATUS_LOG_PATH = path.resolve(__dirname, '..', 'tmp', 'test-status.log');
|
|
process.env.TRIP_STATUS_UPLOAD_DIR = TEST_UPLOAD_DIR;
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual'
|
|
|
|
const app = require('../app');
|
|
const db = require('../src/config/db');
|
|
const tripStatusPhotoStorage = require('../src/services/tripStatusPhotoStorage');
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret';
|
|
process.env.JWT_SECRET = JWT_SECRET;
|
|
|
|
let originalQuery;
|
|
let originalGetConnection;
|
|
|
|
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 requestJson = async ({ port, method, path: requestPath, authorization, body }) =>
|
|
new Promise((resolve, reject) => {
|
|
const rawBody = body ? JSON.stringify(body) : '';
|
|
|
|
const req = http.request(
|
|
{
|
|
hostname: '127.0.0.1',
|
|
port,
|
|
method,
|
|
path: requestPath,
|
|
headers: {
|
|
...(authorization ? { authorization } : {}),
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(rawBody)
|
|
}
|
|
},
|
|
(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(rawBody);
|
|
req.end();
|
|
});
|
|
|
|
const requestMultipart = async ({
|
|
port,
|
|
method,
|
|
path: requestPath,
|
|
authorization,
|
|
fields = {},
|
|
files = []
|
|
}) =>
|
|
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`)
|
|
);
|
|
}
|
|
|
|
for (const file of files) {
|
|
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,
|
|
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 createSftpRecorder = () => ({
|
|
connectCalls: 0,
|
|
mkdirCalls: [],
|
|
putCalls: [],
|
|
deleteCalls: [],
|
|
endCalls: 0
|
|
});
|
|
|
|
const createFakeSftpClientFactory = (recorder, options = {}) => () => ({
|
|
connect: async () => {
|
|
recorder.connectCalls += 1;
|
|
if (options.failConnect) {
|
|
throw new Error('SFTP_CONNECT_FAILED');
|
|
}
|
|
},
|
|
exists: async (remotePath) => {
|
|
const normalizedPath = String(remotePath || '');
|
|
const lastSegment = normalizedPath.split('/').filter(Boolean).pop() || '';
|
|
|
|
// Simulate an existing base dir and a missing per-trip dir (numeric segment).
|
|
return !/^\d+$/.test(lastSegment);
|
|
},
|
|
mkdir: async (remoteDir, recursive) => {
|
|
recorder.mkdirCalls.push({ remoteDir, recursive });
|
|
},
|
|
put: async (localPath, remotePath) => {
|
|
recorder.putCalls.push({ localPath, remotePath });
|
|
if (options.failPut) {
|
|
throw new Error('SFTP_PUT_FAILED');
|
|
}
|
|
},
|
|
delete: async (remotePath) => {
|
|
recorder.deleteCalls.push(remotePath);
|
|
if (options.failDelete) {
|
|
throw new Error('SFTP_DELETE_FAILED');
|
|
}
|
|
},
|
|
end: async () => {
|
|
recorder.endCalls += 1;
|
|
}
|
|
});
|
|
|
|
const waitFor = async (predicate, { timeoutMs = 1500, intervalMs = 25 } = {}) => {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (predicate()) {
|
|
return;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
}
|
|
|
|
throw new Error('Timed out waiting for condition');
|
|
};
|
|
|
|
const readJsonLines = (filePath) =>
|
|
fs
|
|
.readFileSync(filePath, 'utf8')
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line));
|
|
|
|
test.before(() => {
|
|
originalQuery = db.query;
|
|
originalGetConnection = db.getConnection;
|
|
fs.rmSync(TEST_UPLOAD_DIR, { recursive: true, force: true });
|
|
fs.rmSync(TEST_POSTS_LOG_PATH, { force: true });
|
|
fs.rmSync(TEST_STATUS_LOG_PATH, { force: true });
|
|
});
|
|
|
|
test.after(() => {
|
|
db.query = originalQuery;
|
|
db.getConnection = originalGetConnection;
|
|
tripStatusPhotoStorage.__resetSftpClientFactoryForTests();
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
|
delete process.env.TRIP_STATUS_SFTP_HOST;
|
|
delete process.env.TRIP_STATUS_SFTP_PORT;
|
|
delete process.env.TRIP_STATUS_SFTP_USERNAME;
|
|
delete process.env.TRIP_STATUS_SFTP_PASSWORD;
|
|
delete process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR;
|
|
delete process.env.POSTS_LOG_PATH;
|
|
delete process.env.TRIP_STATUS_UPDATES_LOG_PATH;
|
|
fs.rmSync(TEST_UPLOAD_DIR, { recursive: true, force: true });
|
|
fs.rmSync(TEST_POSTS_LOG_PATH, { force: true });
|
|
fs.rmSync(TEST_STATUS_LOG_PATH, { force: true });
|
|
});
|
|
|
|
test.afterEach(() => {
|
|
db.query = originalQuery;
|
|
db.getConnection = originalGetConnection;
|
|
tripStatusPhotoStorage.__resetSftpClientFactoryForTests();
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
|
delete process.env.TRIP_STATUS_SFTP_HOST;
|
|
delete process.env.TRIP_STATUS_SFTP_PORT;
|
|
delete process.env.TRIP_STATUS_SFTP_USERNAME;
|
|
delete process.env.TRIP_STATUS_SFTP_PASSWORD;
|
|
delete process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR;
|
|
delete process.env.POSTS_LOG_PATH;
|
|
delete process.env.TRIP_STATUS_UPDATES_LOG_PATH;
|
|
fs.rmSync(TEST_POSTS_LOG_PATH, { force: true });
|
|
fs.rmSync(TEST_STATUS_LOG_PATH, { force: true });
|
|
});
|
|
|
|
test('POST /api/trips/:id/status está registrado en /api', () => {
|
|
const apiRouterLayers = app._router.stack.filter(
|
|
(layer) =>
|
|
layer.name === 'router' &&
|
|
layer.regexp &&
|
|
layer.regexp.toString().includes('^\\/api\\/?(?=\\/|$)')
|
|
);
|
|
|
|
assert.ok(apiRouterLayers.length > 0, 'Router /api is not mounted');
|
|
|
|
const statusRouteLayer = apiRouterLayers
|
|
.flatMap((routerLayer) => routerLayer.handle.stack)
|
|
.find(
|
|
(layer) =>
|
|
layer.route &&
|
|
layer.route.path === '/trips/:id/status' &&
|
|
layer.route.methods.post
|
|
);
|
|
|
|
assert.ok(statusRouteLayer, 'POST /api/trips/:id/status route is not defined');
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado intermedio 5 con id_punto => OK', async () => {
|
|
let step = 0;
|
|
const legacyPointValue = '151:|:obs previa:|:2026-03-10:|:09:00';
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [5]);
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.deepEqual(params, [248230, '58045340X']);
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
assert.deepEqual(params, [8123, 248230]);
|
|
return [[{ id_punto: 8123, id_estado_intermedio: 4, valor: legacyPointValue, foto: 'previa.png' }]];
|
|
}
|
|
|
|
if (step === 5) {
|
|
assert.match(sql, /UPDATE c_viajes_puntos/);
|
|
assert.equal(params[0], 5);
|
|
assert.equal(params[1], '151:|:ok sin fotos:|:2026-03-10:|:09:00');
|
|
assert.equal(params[2], null);
|
|
assert.equal(params[3], null);
|
|
assert.equal(params[7], 8123);
|
|
assert.equal(params[8], 248230);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
throw new Error(`Unexpected query on step ${step}: ${sql}`);
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
id_punto: 8123,
|
|
observaciones: 'ok sin fotos'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.trip_id, 248230);
|
|
assert.equal(response.body.status_id, 5);
|
|
assert.equal(response.body.id_estado, 5);
|
|
assert.equal(response.body.id_punto, 8123);
|
|
assert.match(response.body.updated_at, /^\d{4}-\d{2}-\d{2}T/);
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status intermedio sin observaciones ni fotos limpia valor y foto previos', async () => {
|
|
let step = 0;
|
|
const legacyPointValue = '151:|:comentario previo:|:2026-03-08:|:10:00';
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 4 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
return [[{
|
|
id_punto: 8123,
|
|
id_estado_intermedio: 3,
|
|
valor: legacyPointValue,
|
|
foto: 'foto-previa.png',
|
|
fecha_foto: '2026-03-08 10:00:00'
|
|
}]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes_puntos/);
|
|
assert.equal(params[0], 4);
|
|
assert.equal(params[1], '151:|::|:2026-03-08:|:10:00');
|
|
assert.equal(params[2], null);
|
|
assert.equal(params[3], null);
|
|
assert.equal(params[7], 8123);
|
|
assert.equal(params[8], 248230);
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 4,
|
|
id_punto: 8123,
|
|
observaciones: ' '
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.trip_id, 248230);
|
|
assert.equal(response.body.id_estado, 4);
|
|
assert.equal(response.body.id_punto, 8123);
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status intermedio multipart con foto conserva ref_punto_id aunque observaciones queden vacias', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
return [[{
|
|
id_punto: 8123,
|
|
id_estado_intermedio: 4,
|
|
valor: '151:|:comentario previo:|:2026-03-08:|:10:00',
|
|
foto: null,
|
|
fecha_foto: null
|
|
}]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes_puntos/);
|
|
assert.equal(params[0], 5);
|
|
assert.equal(params[1], '151:|::|:2026-03-08:|:10:00');
|
|
assert.match(params[2], /^[a-f0-9]{6}\.png$/);
|
|
assert.ok(params[3] instanceof Date);
|
|
assert.equal(params[7], 8123);
|
|
assert.equal(params[8], 248230);
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 5,
|
|
id_punto: 8123,
|
|
observaciones: ' '
|
|
},
|
|
files: [{
|
|
fieldName: 'fotos',
|
|
filename: 'evidence.png',
|
|
contentType: 'image/png',
|
|
content: Buffer.from('png-binary')
|
|
}]
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status multipart registra body parseado y ficheros en posts.log', async () => {
|
|
let step = 0;
|
|
process.env.POSTS_LOG_PATH = TEST_POSTS_LOG_PATH;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [[{ id_punto: 8123, id_estado_intermedio: 4 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes_puntos/);
|
|
assert.equal(params[0], 5);
|
|
assert.equal(params[1], 'desde multipart');
|
|
assert.match(params[2], /^[a-f0-9]{6}\.png$/);
|
|
assert.ok(params[3] instanceof Date);
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 5,
|
|
id_punto: 8123,
|
|
observaciones: 'desde multipart'
|
|
},
|
|
files: [{
|
|
fieldName: 'fotos',
|
|
filename: 'evidence.png',
|
|
contentType: 'image/png',
|
|
content: Buffer.from('png-binary')
|
|
}]
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
await waitFor(() => fs.existsSync(TEST_POSTS_LOG_PATH));
|
|
|
|
const [entry] = readJsonLines(TEST_POSTS_LOG_PATH);
|
|
assert.equal(entry.method, 'POST');
|
|
assert.equal(entry.path, '/api/trips/248230/status');
|
|
assert.equal(entry.body.id_estado, '5');
|
|
assert.equal(entry.body.id_punto, '8123');
|
|
assert.equal(entry.body.observaciones, 'desde multipart');
|
|
assert.equal(entry.raw_body, null);
|
|
assert.equal(entry.files.length, 1);
|
|
assert.equal(entry.files[0].field_name, 'fotos');
|
|
assert.match(entry.files[0].filename, /^[a-f0-9]{6}\.png$/);
|
|
assert.equal(entry.files[0].originalname, 'evidence.png');
|
|
});
|
|
|
|
test('POST /api/trips/:id/status intermedio registra detalle rico en status.log', async () => {
|
|
let step = 0;
|
|
process.env.TRIP_STATUS_UPDATES_LOG_PATH = TEST_STATUS_LOG_PATH;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [[{
|
|
id_punto: 8123,
|
|
id_estado_intermedio: 4,
|
|
valor: 'previo',
|
|
foto: 'previa.png'
|
|
}]];
|
|
}
|
|
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
id_punto: 8123,
|
|
observaciones: 'audit me'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
await waitFor(() => fs.existsSync(TEST_STATUS_LOG_PATH));
|
|
|
|
const entries = readJsonLines(TEST_STATUS_LOG_PATH);
|
|
const entry = entries.find((candidate) => candidate.operation === 'upsert_point_status');
|
|
|
|
assert.ok(entry, 'Expected intermediate point status entry in status.log');
|
|
assert.equal(entry.flow, 'intermediate_point_manual');
|
|
assert.equal(entry.result, 'SUCCESS');
|
|
assert.equal(entry.trip_id, 248230);
|
|
assert.equal(entry.id_punto, 8123);
|
|
assert.equal(entry.id_estado, 5);
|
|
assert.equal(entry.previous_intermediate_status_id, 4);
|
|
assert.equal(entry.new_intermediate_status_id, 5);
|
|
assert.equal(entry.valor_written, 'audit me');
|
|
assert.equal(entry.foto_written, null);
|
|
assert.equal(entry.valor_cleared, false);
|
|
assert.equal(entry.foto_cleared, true);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado 7 sin fotos => 422', async () => {
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 7,
|
|
observaciones: 'sin fotos'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 422);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Photo required for status 7'
|
|
});
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado 7 con múltiples fotos => OK', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 7 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 5) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
|
return [{ insertId: 2, affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 7,
|
|
observaciones: 'entregado',
|
|
fotos_concat: ''
|
|
},
|
|
files: [
|
|
{
|
|
fieldName: 'fotos',
|
|
filename: 'photo1.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-jpeg-1')
|
|
},
|
|
{
|
|
fieldName: 'fotos',
|
|
filename: 'photo2.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-jpeg-2')
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.trip_id, 248230);
|
|
assert.equal(response.body.id_estado, 7);
|
|
assert.match(response.body.fotos_concat, /^[a-f0-9]{6}\.(jpg|png|webp|heic|heif);[a-f0-9]{6}\.(jpg|png|webp|heic|heif)$/);
|
|
const storedPhotos = response.body.fotos_concat.split(';').filter(Boolean);
|
|
assert.equal(storedPhotos.length, 2);
|
|
for (const fileName of storedPhotos) {
|
|
assert.equal(
|
|
fs.existsSync(path.join(TEST_UPLOAD_DIR, '248230', fileName)),
|
|
true
|
|
);
|
|
}
|
|
assert.match(response.body.updated_at, /^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status modo dual replica foto a SFTP y mantiene local', async () => {
|
|
const recorder = createSftpRecorder();
|
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
|
process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR =
|
|
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native';
|
|
|
|
let step = 0;
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 7 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4 || step === 5) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
return [{ insertId: 91, affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 7,
|
|
observaciones: 'modo dual'
|
|
},
|
|
files: [
|
|
{
|
|
fieldName: 'fotos',
|
|
filename: 'dual-photo.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-dual-jpeg')
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
const storedPhotoName = response.body.fotos_concat;
|
|
assert.match(storedPhotoName, /^[a-f0-9]{6}\.(jpg|png|webp|heic|heif)$/);
|
|
assert.equal(
|
|
fs.existsSync(path.join(TEST_UPLOAD_DIR, '248230', storedPhotoName)),
|
|
true
|
|
);
|
|
assert.equal(recorder.connectCalls, 1);
|
|
assert.equal(recorder.mkdirCalls.length, 1);
|
|
assert.equal(
|
|
recorder.mkdirCalls[0].remoteDir,
|
|
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native/248230'
|
|
);
|
|
assert.equal(recorder.putCalls.length, 1);
|
|
assert.equal(
|
|
recorder.putCalls[0].remotePath,
|
|
`/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native/248230/${storedPhotoName}`
|
|
);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status modo dual con fallo SFTP mantiene fallback local', async () => {
|
|
const recorder = createSftpRecorder();
|
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(
|
|
createFakeSftpClientFactory(recorder, { failPut: true })
|
|
);
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
|
process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR =
|
|
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native';
|
|
|
|
let step = 0;
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 7 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4 || step === 5) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
return [{ insertId: 92, affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 7,
|
|
observaciones: 'fallback local'
|
|
},
|
|
files: [
|
|
{
|
|
fieldName: 'fotos',
|
|
filename: 'dual-fallback-photo.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-dual-fallback-jpeg')
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
const storedPhotoName = response.body.fotos_concat;
|
|
assert.match(storedPhotoName, /^[a-f0-9]{6}\.(jpg|png|webp|heic|heif)$/);
|
|
assert.equal(
|
|
fs.existsSync(path.join(TEST_UPLOAD_DIR, '248230', storedPhotoName)),
|
|
true
|
|
);
|
|
assert.equal(recorder.putCalls.length, 1);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status payload inválido tras upload limpia remoto y local en modo dual', async () => {
|
|
const invalidPayloadTripId = 248299;
|
|
const recorder = createSftpRecorder();
|
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
|
process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR =
|
|
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native';
|
|
const localTripDir = path.join(TEST_UPLOAD_DIR, String(invalidPayloadTripId));
|
|
fs.rmSync(localTripDir, { recursive: true, force: true });
|
|
|
|
db.query = async () => {
|
|
throw new Error('db.query should not run for invalid payload');
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: `/api/trips/${invalidPayloadTripId}/status`,
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
observaciones: 'sin id_estado'
|
|
},
|
|
files: [
|
|
{
|
|
fieldName: 'fotos',
|
|
filename: 'dual-invalid-photo.jpg',
|
|
contentType: 'image/jpeg',
|
|
content: Buffer.from('fake-dual-invalid-jpeg')
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 400);
|
|
assert.equal(recorder.putCalls.length, 1);
|
|
await waitFor(() => recorder.deleteCalls.length === 1);
|
|
const localFiles = fs.existsSync(localTripDir) ? fs.readdirSync(localTripDir) : [];
|
|
assert.equal(localFiles.length, 0);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado 7 con fotos_concat (sin archivo) => OK', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 7 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 5) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
return [{ insertId: 3, affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 7,
|
|
observaciones: 'entregado con fallback',
|
|
fotos_concat: 'legacy1.jpg;legacy2.jpg'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.id_estado, 7);
|
|
assert.equal(response.body.fotos_concat, 'legacy1.jpg;legacy2.jpg');
|
|
});
|
|
|
|
test('POST /api/trips/:id/status permite rollback 7 -> 6', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 196854 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 5) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
return [{ insertId: 4, affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/196854/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6,
|
|
observaciones: 'rollback a 6'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.id_estado, 6);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado 5 sin id_punto usa flujo global legacy => 200', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [5]);
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.deepEqual(params, [196854]);
|
|
return [[{ id_viaje: 196854 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.deepEqual(params, [196854, '58045340X']);
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [5, 1, 196854]);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 5) {
|
|
assert.match(sql, /UPDATE c_viajes_proveedor/);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
|
return [{ insertId: 6, affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/196854/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
observaciones: 'falta id_punto'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.trip_id, 196854);
|
|
assert.equal(response.body.id_estado, 5);
|
|
assert.equal(step, 6);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status propaga estado global al viaje padre', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [6]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_viaje: 248230, id_viaje_padre: 196854 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.deepEqual(params, [248230, '58045340X']);
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.match(sql, /ind_edi_app = 1/);
|
|
assert.deepEqual(params, [6, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 5) {
|
|
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
|
assert.equal(params[0], 248230);
|
|
assert.equal(params[3], 6);
|
|
return [{ insertId: 6, affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 6) {
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.match(sql, /ind_edi_app = 1/);
|
|
assert.deepEqual(params, [6, 1, 196854]);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (step === 7) {
|
|
assert.match(sql, /SELECT id_viaje_padre/);
|
|
assert.deepEqual(params, [196854]);
|
|
return [[{ id_viaje_padre: 0 }]];
|
|
}
|
|
|
|
throw new Error(`Unexpected query step ${step}: ${sql}`);
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.trip_id, 248230);
|
|
assert.equal(response.body.id_estado, 6);
|
|
assert.equal(step, 7);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado intermedio con id_punto inválido => 400', async () => {
|
|
db.query = async () => {
|
|
throw new Error('db.query should not run for invalid id_punto');
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/196854/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
id_punto: 'abc'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 400);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Invalid payload'
|
|
});
|
|
});
|
|
|
|
test('POST /api/trips/:id/status estado intermedio con id_punto inexistente => 404', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [5]);
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.deepEqual(params, [248230, '58045340X']);
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
assert.deepEqual(params, [99999, 248230]);
|
|
return [[]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
id_punto: 99999,
|
|
observaciones: 'punto inválido'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 404);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Trip point not found'
|
|
});
|
|
assert.equal(step, 4);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status devuelve 401 sin token', async () => {
|
|
db.query = async () => {
|
|
throw new Error('db.query should not be called without token');
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 401);
|
|
assert.deepEqual(response.body, { error: 'Unauthorized' });
|
|
});
|
|
|
|
test('POST /api/trips/:id/status devuelve 403 para viaje no autorizado', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248080 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[]];
|
|
}
|
|
|
|
throw new Error('No update query should run for forbidden trip');
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248080/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 403);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Forbidden'
|
|
});
|
|
});
|
|
|
|
test('POST /api/trips/:id/status devuelve 404 cuando viaje no existe', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[]];
|
|
}
|
|
|
|
throw new Error('Authorization query should not run when trip does not exist');
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/99999999/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 404);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Trip not found'
|
|
});
|
|
});
|
|
|
|
|
|
test('POST /api/trips/:id/status ind_fallido=1 actualiza estado existente X y agrega 9 => OK', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
let beginCalled = false;
|
|
let commitCalled = false;
|
|
let rollbackCalled = false;
|
|
let releaseCalled = false;
|
|
|
|
db.query = async (sql, params) => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [6]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [9]);
|
|
return [[{ id_estado: 9 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {
|
|
beginCalled = true;
|
|
},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_viaje: 248230, id_estado: 6, ind_fallido: 0 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
assert.deepEqual(params, [248230, '58045340X']);
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /UPDATE c_viajes_proveedor/);
|
|
assert.deepEqual(params, ['fallido en estado 6', 248230, '58045340X']);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 4) {
|
|
assert.match(sql, /UPDATE c_cambios_estado/);
|
|
assert.equal(params[9], 6);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 5) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
assert.match(sql, /ORDER BY fecha_y_hora DESC/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 6) {
|
|
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
|
assert.equal(params[3], 9);
|
|
assert.equal(params[4], 0);
|
|
return [{ insertId: 20, affectedRows: 1 }];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [9, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {
|
|
commitCalled = true;
|
|
},
|
|
rollback: async () => {
|
|
rollbackCalled = true;
|
|
},
|
|
release: () => {
|
|
releaseCalled = true;
|
|
}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 6,
|
|
ind_fallido: 1,
|
|
observaciones: 'fallido en estado 6'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.trip_id, 248230);
|
|
assert.equal(response.body.updated_status_id, 6);
|
|
assert.equal(response.body.inserted_failed_status_id, 9);
|
|
assert.equal(response.body.action_taken, 'update+insert_failed');
|
|
assert.equal(response.body.id_estado, 9);
|
|
assert.equal(response.body.failed_marked, true);
|
|
assert.equal(beginCalled, true);
|
|
assert.equal(commitCalled, true);
|
|
assert.equal(rollbackCalled, false);
|
|
assert.equal(releaseCalled, true);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status ind_fallido=1 inserta X si no existe y luego inserta 9 => OK', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [6]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [9]);
|
|
return [[{ id_estado: 9 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
return [[{ id_viaje: 248230, id_estado: 6, ind_fallido: 0 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /UPDATE c_cambios_estado/);
|
|
assert.equal(params[9], 6);
|
|
return [{ affectedRows: 0 }];
|
|
}
|
|
|
|
if (txStep === 4) {
|
|
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
|
assert.equal(params[3], 6);
|
|
assert.equal(params[4], 1);
|
|
return [{ insertId: 21, affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 5) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 6) {
|
|
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
|
assert.equal(params[3], 9);
|
|
assert.equal(params[4], 0);
|
|
return [{ insertId: 22, affectedRows: 1 }];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [9, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not be called for successful transaction');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 6,
|
|
ind_fallido: 1
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.updated_status_id, 6);
|
|
assert.equal(response.body.inserted_failed_status_id, 9);
|
|
assert.equal(response.body.action_taken, 'insert+insert_failed');
|
|
assert.equal(response.body.message, 'Estado marcado como fallido y estado 9 registrado');
|
|
});
|
|
|
|
test('POST /api/trips/:id/status ind_fallido=1 evita 9 consecutivo si el último ya es 9', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [6]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [9]);
|
|
return [[{ id_estado: 9 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
return [[{ id_viaje: 248230, id_estado: 9, ind_fallido: 1 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /UPDATE c_cambios_estado/);
|
|
assert.equal(params[9], 6);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 4) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
return [[{ id_estado: 9 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [9, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not be called for successful transaction');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 6,
|
|
ind_fallido: 1
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.updated_status_id, 6);
|
|
assert.equal(response.body.inserted_failed_status_id, null);
|
|
assert.equal(response.body.action_taken, 'update+skip_failed_insert');
|
|
assert.equal(
|
|
response.body.message,
|
|
'Estado marcado como fallido; no se insertó 9 porque ya era el último estado'
|
|
);
|
|
});
|
|
|
|
test('POST /api/trips/:id/status ind_fallido=1 hace rollback ante error intermedio', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
let commitCalled = false;
|
|
let rollbackCalled = false;
|
|
let releaseCalled = false;
|
|
|
|
db.query = async (sql, params) => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [6]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [9]);
|
|
return [[{ id_estado: 9 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
return [[{ id_viaje: 248230, id_estado: 6, ind_fallido: 0 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
return [[{ n_proveedor: 1, id_proveedor: 675 }]];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 4) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 5) {
|
|
return [{ insertId: 23, affectedRows: 1 }];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
throw new Error('forced update failure');
|
|
},
|
|
commit: async () => {
|
|
commitCalled = true;
|
|
},
|
|
rollback: async () => {
|
|
rollbackCalled = true;
|
|
},
|
|
release: () => {
|
|
releaseCalled = true;
|
|
}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestMultipart({
|
|
port: server.address().port,
|
|
method: 'POST',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
fields: {
|
|
id_estado: 6,
|
|
ind_fallido: 1
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 500);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Internal server error'
|
|
});
|
|
assert.equal(commitCalled, false);
|
|
assert.equal(rollbackCalled, true);
|
|
assert.equal(releaseCalled, true);
|
|
});
|
|
|
|
test('GET /api/trips/:id/status-history combina global + intermedios con status_key', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.deepEqual(params, [248230, '58045340X']);
|
|
return [[{ ok: 1 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM c_cambios_estado/);
|
|
assert.match(sql, /UNION ALL/);
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
assert.match(sql, /COALESCE\(c\.actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.match(sql, /COALESCE\(vp\.actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.deepEqual(params, [248230, 248230, 3, 4, 5]);
|
|
|
|
return [[
|
|
{ id_estado: 1, fecha_hora: '2026-02-17 08:00:00', id_punto: null },
|
|
{ id_estado: 3, fecha_hora: '2026-02-17 08:10:00', id_punto: null },
|
|
{ id_estado: 3, fecha_hora: '2026-02-17 08:20:00', id_punto: 8123 },
|
|
{ id_estado: 4, fecha_hora: '2026-02-17 08:30:00', id_punto: 8123 },
|
|
{ id_estado: 5, fecha_hora: '2026-02-17 08:40:00', id_punto: 8123 },
|
|
{ id_estado: 6, fecha_hora: '2026-02-17 09:00:00', id_punto: null },
|
|
{ id_estado: 7, fecha_hora: '2026-02-17 09:30:00', id_punto: null }
|
|
]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'GET',
|
|
path: '/api/trips/248230/status-history',
|
|
authorization: `Bearer ${createToken()}`
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
history: [
|
|
{ id_estado: 1, fecha_hora: '2026-02-17 08:00:00', id_punto: null, status_key: 'assigned' },
|
|
{ id_estado: 3, fecha_hora: '2026-02-17 08:10:00', id_punto: null, status_key: 'positioned' },
|
|
{
|
|
id_estado: 3,
|
|
fecha_hora: '2026-02-17 08:20:00',
|
|
id_punto: 8123,
|
|
status_key: 'point_8123_positioned'
|
|
},
|
|
{
|
|
id_estado: 4,
|
|
fecha_hora: '2026-02-17 08:30:00',
|
|
id_punto: 8123,
|
|
status_key: 'point_8123_loading_unloading'
|
|
},
|
|
{
|
|
id_estado: 5,
|
|
fecha_hora: '2026-02-17 08:40:00',
|
|
id_punto: 8123,
|
|
status_key: 'point_8123_transit'
|
|
},
|
|
{ id_estado: 6, fecha_hora: '2026-02-17 09:00:00', id_punto: null, status_key: 'arrival' },
|
|
{ id_estado: 7, fecha_hora: '2026-02-17 09:30:00', id_punto: null, status_key: 'closed' }
|
|
]
|
|
});
|
|
assert.equal(step, 3);
|
|
});
|
|
|
|
test('GET /api/trips/:id/status-history mantiene status_key para eventos manuales', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ ok: 1 }]];
|
|
}
|
|
|
|
assert.match(sql, /COALESCE\(c\.actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.match(sql, /COALESCE\(vp\.actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.deepEqual(params, [248230, 248230, 3, 4, 5]);
|
|
|
|
return [[
|
|
{ id_estado: 3, fecha_hora: '2026-02-17 08:10:00', id_punto: null },
|
|
{ id_estado: 5, fecha_hora: '2026-02-17 08:40:00', id_punto: 8123 }
|
|
]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'GET',
|
|
path: '/api/trips/248230/status-history',
|
|
authorization: `Bearer ${createToken()}`
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
history: [
|
|
{ id_estado: 3, fecha_hora: '2026-02-17 08:10:00', id_punto: null, status_key: 'positioned' },
|
|
{
|
|
id_estado: 5,
|
|
fecha_hora: '2026-02-17 08:40:00',
|
|
id_punto: 8123,
|
|
status_key: 'point_8123_transit'
|
|
}
|
|
]
|
|
});
|
|
assert.equal(step, 3);
|
|
});
|
|
|
|
test('GET /api/trips/:id/status-history devuelve 403 para viaje no autorizado', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
if (step === 1) {
|
|
return [[{ id_viaje: 248230 }]];
|
|
}
|
|
|
|
return [[]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'GET',
|
|
path: '/api/trips/248230/status-history',
|
|
authorization: `Bearer ${createToken()}`
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 403);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Forbidden'
|
|
});
|
|
assert.equal(step, 2);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio 5 retrocede a 4', async () => {
|
|
let step = 0;
|
|
|
|
db.getConnection = async () => {
|
|
throw new Error('transaction should not run for intermediate point delete');
|
|
};
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
assert.match(sql, /FROM t_viaje_estados/);
|
|
assert.deepEqual(params, [5]);
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
assert.match(sql, /FROM c_viajes/);
|
|
assert.deepEqual(params, [248230]);
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
assert.match(sql, /FROM c_viajes_proveedor/);
|
|
assert.deepEqual(params, [248230, '58045340X']);
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s+AS\s+actualizado_automaticamente/);
|
|
assert.deepEqual(params, [8123, 248230]);
|
|
return [[{
|
|
id_punto: 8123,
|
|
id_estado_intermedio: 5,
|
|
valor: '151:|:comentario previo:|:2026-03-10:|:09:00',
|
|
actualizado_automaticamente: 0
|
|
}]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes_puntos/);
|
|
assert.match(sql, /borrado_en_app = 1/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.deepEqual(params, [4, '151:|::|:2026-03-10:|:09:00', 8123, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
id_punto: 8123
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 5,
|
|
new_intermediate_status_id: 4,
|
|
current_status_id: 6,
|
|
id_punto: 8123
|
|
});
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio limpia valor foto y fecha_foto y registra status.log', async () => {
|
|
let step = 0;
|
|
process.env.TRIP_STATUS_UPDATES_LOG_PATH = TEST_STATUS_LOG_PATH;
|
|
|
|
db.query = async (sql, params) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [[{
|
|
id_punto: 8123,
|
|
id_estado_intermedio: 5,
|
|
valor: '151:|:comentario previo:|:2026-03-10:|:09:00',
|
|
actualizado_automaticamente: 0
|
|
}]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes_puntos/);
|
|
assert.match(sql, /valor = \?/);
|
|
assert.match(sql, /foto = NULL/);
|
|
assert.match(sql, /fecha_foto = NULL/);
|
|
assert.deepEqual(params, [4, '151:|::|:2026-03-10:|:09:00', 8123, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 5,
|
|
id_punto: 8123
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
await waitFor(() => fs.existsSync(TEST_STATUS_LOG_PATH));
|
|
|
|
const entries = readJsonLines(TEST_STATUS_LOG_PATH);
|
|
const entry = entries.find((candidate) => candidate.operation === 'clear_point_status');
|
|
|
|
assert.ok(entry, 'Expected clear_point_status entry in status.log');
|
|
assert.equal(entry.result, 'ROLLBACK_APPLIED');
|
|
assert.equal(entry.previous_intermediate_status_id, 5);
|
|
assert.equal(entry.new_intermediate_status_id, 4);
|
|
assert.equal(entry.valor_cleared, true);
|
|
assert.equal(entry.foto_cleared, true);
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio 4 retrocede a 3', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 4 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [[{ id_punto: 8123, id_estado_intermedio: 4 }]];
|
|
}
|
|
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 4,
|
|
id_punto: 8123
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 4,
|
|
new_intermediate_status_id: 3,
|
|
current_status_id: 6,
|
|
id_punto: 8123
|
|
});
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio 3 retrocede a NULL', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 3 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
if (step === 4) {
|
|
return [[{ id_punto: 8123, id_estado_intermedio: 3 }]];
|
|
}
|
|
|
|
return [{ affectedRows: 1 }];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 3,
|
|
id_punto: 8123
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 3,
|
|
new_intermediate_status_id: null,
|
|
current_status_id: 6,
|
|
id_punto: 8123
|
|
});
|
|
assert.equal(step, 5);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio devuelve 404 si el punto no tiene estado intermedio válido', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 3 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
return [[{ id_punto: 8123, id_estado_intermedio: null }]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 3,
|
|
id_punto: 8123
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 404);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Trip point intermediate status not found'
|
|
});
|
|
assert.equal(step, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio ignora estado automatico y devuelve 404', async () => {
|
|
let step = 0;
|
|
|
|
db.getConnection = async () => {
|
|
throw new Error('transaction should not run for intermediate point delete');
|
|
};
|
|
|
|
db.query = async (sql) => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 3 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
assert.match(sql, /FROM c_viajes_puntos/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s+AS\s+actualizado_automaticamente/);
|
|
return [[{
|
|
id_punto: 8123,
|
|
id_estado_intermedio: 3,
|
|
actualizado_automaticamente: 1
|
|
}]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 3,
|
|
id_punto: 8123
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 404);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Trip point intermediate status not found'
|
|
});
|
|
assert.equal(step, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status estado 3 sin id_punto usa flujo global legacy', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async () => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
return [[{ id_estado: 3 }]];
|
|
}
|
|
|
|
if (validationStep === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
return [[{ n_proveedor: 1 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
assert.match(sql, /FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 3]);
|
|
return [[{ id_estado: 3 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
assert.match(sql, /DELETE FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.equal(/LIMIT\s+1/i.test(sql), false);
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 3]);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
return [[{ id_estado: 2 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [2, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not run in successful global clear test');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 3
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 3,
|
|
current_status_id: 2
|
|
});
|
|
assert.equal(validationStep, 3);
|
|
assert.equal(txStep, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status global devuelve 404 cuando no hay cambios manuales elegibles', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
let rollbackCalls = 0;
|
|
|
|
db.query = async () => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (validationStep === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
return [[{ n_proveedor: 1 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql) => {
|
|
txStep += 1;
|
|
assert.equal(txStep, 1);
|
|
assert.match(sql, /FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
return [[]];
|
|
},
|
|
commit: async () => {
|
|
throw new Error('commit should not run when no manual status exists');
|
|
},
|
|
rollback: async () => {
|
|
rollbackCalls += 1;
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 404);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Trip status change not found'
|
|
});
|
|
assert.equal(validationStep, 3);
|
|
assert.equal(txStep, 1);
|
|
assert.equal(rollbackCalls, 1);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio con id_punto inválido => 400', async () => {
|
|
db.query = async () => {
|
|
throw new Error('db.query should not run for invalid id_punto');
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 3,
|
|
id_punto: 'abc'
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 400);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Invalid payload'
|
|
});
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status intermedio devuelve 404 si punto no existe', async () => {
|
|
let step = 0;
|
|
|
|
db.query = async () => {
|
|
step += 1;
|
|
|
|
if (step === 1) {
|
|
return [[{ id_estado: 3 }]];
|
|
}
|
|
|
|
if (step === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
if (step === 3) {
|
|
return [[{ n_proveedor: 1 }]];
|
|
}
|
|
|
|
return [[]];
|
|
};
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 3,
|
|
id_punto: 99999
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 404);
|
|
assert.deepEqual(response.body, {
|
|
success: false,
|
|
error: 'Trip point not found'
|
|
});
|
|
assert.equal(step, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status no intermedio mantiene flujo de c_cambios_estado', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async () => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (validationStep === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
return [[{ n_proveedor: 1 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
assert.match(sql, /FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
assert.match(sql, /DELETE FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.equal(/LIMIT\s+1/i.test(sql), false);
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 6]);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [5, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not run in successful clear test');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 6,
|
|
current_status_id: 5
|
|
});
|
|
assert.equal(validationStep, 3);
|
|
assert.equal(txStep, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status no intermedio borra todas las coincidencias manuales del estado', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async () => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (validationStep === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
return [[{ n_proveedor: 1 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
assert.match(sql, /FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 6]);
|
|
return [[{ id_estado: 6 }, { id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
assert.match(sql, /DELETE FROM c_cambios_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
assert.equal(/LIMIT\s+1/i.test(sql), false);
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 6]);
|
|
return [{ affectedRows: 2 }];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
return [[{ id_estado: 4 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [4, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not run in successful mass clear test');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 6,
|
|
current_status_id: 4
|
|
});
|
|
assert.equal(validationStep, 3);
|
|
assert.equal(txStep, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status en modo dual no borra foto en remoto ni local', async () => {
|
|
const recorder = createSftpRecorder();
|
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
|
process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR =
|
|
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native';
|
|
|
|
const localTripDir = path.join(TEST_UPLOAD_DIR, '248230');
|
|
const localPhotoName = 'abc123.jpg';
|
|
const localPhotoPath = path.join(localTripDir, localPhotoName);
|
|
fs.mkdirSync(localTripDir, { recursive: true });
|
|
fs.writeFileSync(localPhotoPath, 'fake-photo-content');
|
|
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async () => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (validationStep === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
return [[{ n_proveedor: 1 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
assert.match(sql, /FROM c_cambios_estado/);
|
|
assert.match(sql, /FOR UPDATE/);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
assert.match(sql, /DELETE FROM c_cambios_estado/);
|
|
assert.equal(/LIMIT\s+1/i.test(sql), false);
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 6]);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
return [[{ id_estado: 5 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [5, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not run in successful dual clear test');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.success, true);
|
|
assert.equal(response.body.current_status_id, 5);
|
|
assert.equal(fs.existsSync(localPhotoPath), true);
|
|
assert.equal(recorder.connectCalls, 0);
|
|
assert.equal(recorder.deleteCalls.length, 0);
|
|
assert.equal(validationStep, 3);
|
|
assert.equal(txStep, 4);
|
|
});
|
|
|
|
test('DELETE /api/trips/:id/status recalculo ignora cambios automaticos para current_status_id', async () => {
|
|
let validationStep = 0;
|
|
let txStep = 0;
|
|
|
|
db.query = async () => {
|
|
validationStep += 1;
|
|
|
|
if (validationStep === 1) {
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (validationStep === 2) {
|
|
return [[{ id_viaje: 248230, id_estado: 6 }]];
|
|
}
|
|
|
|
return [[{ n_proveedor: 1 }]];
|
|
};
|
|
|
|
const connection = {
|
|
beginTransaction: async () => {},
|
|
query: async (sql, params) => {
|
|
txStep += 1;
|
|
|
|
if (txStep === 1) {
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 6]);
|
|
return [[{ id_estado: 6 }]];
|
|
}
|
|
|
|
if (txStep === 2) {
|
|
assert.deepEqual(params, [248230, 1, '58045340X', 6]);
|
|
assert.equal(/LIMIT\s+1/i.test(sql), false);
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
if (txStep === 3) {
|
|
assert.match(sql, /SELECT id_estado/);
|
|
assert.match(sql, /COALESCE\(actualizado_automaticamente,\s*0\)\s*=\s*0/);
|
|
return [[{ id_estado: 4 }]];
|
|
}
|
|
|
|
assert.match(sql, /UPDATE c_viajes/);
|
|
assert.deepEqual(params, [4, 1, 248230]);
|
|
return [{ affectedRows: 1 }];
|
|
},
|
|
commit: async () => {},
|
|
rollback: async () => {
|
|
throw new Error('rollback should not run in recalculo manual clear test');
|
|
},
|
|
release: () => {}
|
|
};
|
|
|
|
db.getConnection = async () => connection;
|
|
|
|
const response = await withServer(async (server) =>
|
|
requestJson({
|
|
port: server.address().port,
|
|
method: 'DELETE',
|
|
path: '/api/trips/248230/status',
|
|
authorization: `Bearer ${createToken()}`,
|
|
body: {
|
|
id_estado: 6
|
|
}
|
|
})
|
|
);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.body, {
|
|
success: true,
|
|
trip_id: 248230,
|
|
removed_status_id: 6,
|
|
current_status_id: 4
|
|
});
|
|
assert.equal(validationStep, 3);
|
|
assert.equal(txStep, 4);
|
|
});
|