prix-r9 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/import-curl.js +110 -0
- package/index.js +352 -0
- package/package.json +45 -0
- package/promptInforme.txt +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Prix-R9 — Pruebas de Estrés HTTP para Node.js
|
|
2
|
+
|
|
3
|
+
Herramienta CLI ligera para pruebas de carga y rendimiento de APIs REST. Permite realizar envíos concurrentes de alto volumen simulando usuarios simultáneos con escalado progresivo (**Ramp-Up**) e inyección de **variables dinámicas** para evitar colisiones de datos en bases de datos.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Instalación
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g prix-r9
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Cómo Empezar
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
prix-r9 --config mi-config.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Toda la configuración (URL, cabeceras, métodos y tiempos) reside dentro del archivo JSON que le pases como parámetro. Esto te permite guardar un historial de todos los servicios estresados.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Importador Automático desde cURL
|
|
26
|
+
|
|
27
|
+
No tienes que escribir el archivo JSON a mano. Si tienes una petición que ya funciona en **Postman, Swagger o en tu Navegador**, simplemente cópiala como formato `cURL` y la herramienta creará el JSON por ti.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Guarda tu comando cURL en un archivo de texto
|
|
31
|
+
# (Click derecho en Network tab del navegador → Copy as cURL)
|
|
32
|
+
|
|
33
|
+
# 2. Convierte a config JSON
|
|
34
|
+
prix-r9-curl -i mi-curl.txt -o casos/mi-endpoint.json
|
|
35
|
+
|
|
36
|
+
# 3. Lanza la prueba
|
|
37
|
+
prix-r9 --config casos/mi-endpoint.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**¿Qué hace?**
|
|
41
|
+
- Extrae la URL y el Método HTTP automáticamente.
|
|
42
|
+
- Extrae todas las cabeceras (Tokens, ApiKeys).
|
|
43
|
+
- Detecta si es un envío JSON regular (`--data`) o un envío de Archivo pesado (`--form`). Si es archivo, automáticamente separa la ruta física local (`file` / `filekey`) y construye el `body` con los campos extra.
|
|
44
|
+
- Le inyecta un **Ramp-Up predeterminado** (5 a 50 req/s en 5 segundos), listo para que lo afines.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Organización por Casos de Prueba (Recomendado)
|
|
49
|
+
|
|
50
|
+
Para no perder la configuración de un WebService que probaste y que podrías volver a ocupar en meses, guarda los archivos JSON divididos por carpetas:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
mi-proyecto/
|
|
54
|
+
└── casos/
|
|
55
|
+
├── modulo-usuarios/
|
|
56
|
+
│ └── crear-usuario.json
|
|
57
|
+
├── modulo-reportes/
|
|
58
|
+
│ └── generar-reporte.json
|
|
59
|
+
└── modulo-archivos/
|
|
60
|
+
└── upload-csv.json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **IMPORTANTE:** Tú y tu equipo deciden cómo nombrar las carpetas. Pueden ser por Producto, Módulo, Ambiente de QA o Sprint.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Motor de Carga (Rate & Ramp-Up)
|
|
68
|
+
|
|
69
|
+
La herramienta permite moldear cómo las peticiones van entrando a tu API a través del tiempo usando cuatro valores clave:
|
|
70
|
+
|
|
71
|
+
* **`startRate`** (Ej: `5`): Peticiones por segundo al inicio de la prueba.
|
|
72
|
+
* **`targetRate`** (Ej: `50`): Máximo de peticiones por segundo al que se quiere llegar.
|
|
73
|
+
* **`rampUpTime`** (Ej: `5`): Segundos que toma escalar de `startRate` a `targetRate`.
|
|
74
|
+
* **`duration`** (Ej: `10`): Duración total de la prueba en segundos.
|
|
75
|
+
|
|
76
|
+
### Fórmula de cálculo por segundo
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Si s < rampUpTime:
|
|
80
|
+
rate(s) = PISO( startRate + (targetRate - startRate) * (s / rampUpTime) )
|
|
81
|
+
|
|
82
|
+
Si s >= rampUpTime:
|
|
83
|
+
rate(s) = targetRate
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Ejemplo con `startRate: 3, targetRate: 5, rampUpTime: 10, duration: 15`:**
|
|
87
|
+
|
|
88
|
+
| Segundo (s) | Fracción = s ÷ 10 | Incremento = (5-3) × fracción | rate = 3 + incremento | Redondeado |
|
|
89
|
+
|:-----------:|:-----------------:|:-----------------------------:|:---------------------:|:----------:|
|
|
90
|
+
| 1 | 0.1 | 0.2 | 3.2 | **3** |
|
|
91
|
+
| 2 | 0.2 | 0.4 | 3.4 | **3** |
|
|
92
|
+
| 3 | 0.3 | 0.6 | 3.6 | **3** |
|
|
93
|
+
| 4 | 0.4 | 0.8 | 3.8 | **3** |
|
|
94
|
+
| 5 | 0.5 | 1.0 | 4.0 | **4** |
|
|
95
|
+
| 6 | 0.6 | 1.2 | 4.2 | **4** |
|
|
96
|
+
| 7 | 0.7 | 1.4 | 4.4 | **4** |
|
|
97
|
+
| 8 | 0.8 | 1.6 | 4.6 | **4** |
|
|
98
|
+
| 9 | 0.9 | 1.8 | 4.8 | **4** |
|
|
99
|
+
| 10 | 1.0 | 2.0 | 5.0 | **5** |
|
|
100
|
+
| 11 – 15 | — | tope alcanzado | 5 pet/s × 5 seg | **25** |
|
|
101
|
+
| **TOTAL** | | | | **62** |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Variables Dinámicas `{{ }}`
|
|
106
|
+
|
|
107
|
+
Para evitar colisiones por restricciones `UNIQUE` en base de datos, puedes usar variables dinámicas en el `body` y `headers`:
|
|
108
|
+
|
|
109
|
+
- **`{{uuid}}`**: Cadena alfanumérica única (ej: `123e4567-e89b-12d3...`).
|
|
110
|
+
- **`{{timestamp}}`**: Tiempo actual en milisegundos (ej: `1702939129124`).
|
|
111
|
+
- **`{{random_number}}`**: Número aleatorio del 0 al 9999.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Ejemplo 1: Carga JSON
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"url": "https://api.tuempresa.com/v1/usuarios",
|
|
120
|
+
"method": "POST",
|
|
121
|
+
"startRate": 5,
|
|
122
|
+
"targetRate": 50,
|
|
123
|
+
"rampUpTime": 5,
|
|
124
|
+
"duration": 10,
|
|
125
|
+
"headers": {
|
|
126
|
+
"Authorization": "Bearer tu_token",
|
|
127
|
+
"X-Request-ID": "{{uuid}}"
|
|
128
|
+
},
|
|
129
|
+
"body": {
|
|
130
|
+
"id_unico": "{{uuid}}",
|
|
131
|
+
"correo_usuario": "prueba_{{timestamp}}@empresa.com",
|
|
132
|
+
"edad": "{{random_number}}"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Ejemplo 2: Subida de Archivos (Multipart)
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"url": "https://api.tuempresa.com/v1/upload-foto",
|
|
144
|
+
"method": "POST",
|
|
145
|
+
"startRate": 2,
|
|
146
|
+
"targetRate": 10,
|
|
147
|
+
"rampUpTime": 2,
|
|
148
|
+
"duration": 5,
|
|
149
|
+
"headers": {
|
|
150
|
+
"Authorization": "Bearer tu_token"
|
|
151
|
+
},
|
|
152
|
+
"file": "./foto-pesada.jpg",
|
|
153
|
+
"filekey": "foto",
|
|
154
|
+
"body": {
|
|
155
|
+
"TypeFile": "1",
|
|
156
|
+
"ReasonProcess": "Carga desde V{{random_number}}"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- **`file`:** Ruta del archivo a adjuntar en cada petición.
|
|
162
|
+
- **`filekey`:** Nombre del campo que espera el backend (el `name` del `<input type="file">`).
|
|
163
|
+
- **`body`:** Campos extra que acompañan al archivo en el `FormData`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Métricas Generadas
|
|
168
|
+
|
|
169
|
+
Al terminar la prueba se imprime un resumen y se guarda en `reporte.txt`:
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
===========================================
|
|
173
|
+
RESUMEN DE LA PRUEBA
|
|
174
|
+
===========================================
|
|
175
|
+
Peticiones Totales : 410
|
|
176
|
+
Conexiones Máximas : 50
|
|
177
|
+
Rendimiento / Rate : 23.50 Req/s
|
|
178
|
+
|
|
179
|
+
DESGLOSE DE RESULTADOS:
|
|
180
|
+
Exitosas (2xx) : 410
|
|
181
|
+
Errores Cliente (4xx): 0
|
|
182
|
+
Errores Server (5xx): 0
|
|
183
|
+
|
|
184
|
+
TIEMPOS DE RESPUESTA (Latencia en ms):
|
|
185
|
+
Mínimo : 60.88 ms
|
|
186
|
+
Máximo : 165.25 ms
|
|
187
|
+
Promedio: 75.93 ms
|
|
188
|
+
P95 : 88.99 ms
|
|
189
|
+
|
|
190
|
+
CÓDIGOS DE ESTADO HTTP:
|
|
191
|
+
[200] -> 410 veces
|
|
192
|
+
===========================================
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
> **P95**: El 95% de las peticiones tardó igual o menos que ese tiempo.
|
|
196
|
+
> **Rendimiento**: Throughput real (Req/s) que el servidor procesó durante toda la prueba.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Template para Informe con IA
|
|
201
|
+
|
|
202
|
+
El paquete incluye el archivo `promptInforme.txt`, un template profesional para generar informes técnicos de rendimiento usando cualquier IA (ChatGPT, Claude, etc.).
|
|
203
|
+
|
|
204
|
+
**Cómo usarlo:**
|
|
205
|
+
1. Corre uno o varios escenarios de estrés con `prix-r9`
|
|
206
|
+
2. Copia el contenido de cada `reporte.txt` generado
|
|
207
|
+
3. Abre `promptInforme.txt` y pega los reportes en las secciones marcadas
|
|
208
|
+
4. Envía el prompt completo a tu IA preferida
|
|
209
|
+
|
|
210
|
+
El informe generado incluirá: resumen ejecutivo, tabla comparativa, análisis por escenario, diagnóstico del servidor, recomendaciones y semáforo de salud del sistema.
|
|
211
|
+
|
|
212
|
+
El archivo se encuentra en:
|
|
213
|
+
```
|
|
214
|
+
node_modules/prix-r9/promptInforme.txt
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Referencia Rápida
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# Instalar globalmente
|
|
223
|
+
npm install -g prix-r9
|
|
224
|
+
|
|
225
|
+
# Convertir cURL a config JSON
|
|
226
|
+
prix-r9-curl -i mi-curl.txt -o casos/endpoint.json
|
|
227
|
+
|
|
228
|
+
# Ejecutar prueba de estrés
|
|
229
|
+
prix-r9 --config casos/endpoint.json
|
|
230
|
+
|
|
231
|
+
# Ver versión
|
|
232
|
+
prix-r9 --version
|
|
233
|
+
```
|
package/import-curl.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { program } = require('commander');
|
|
5
|
+
const { toJsonString } = require('curlconverter');
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.version('1.0.0')
|
|
9
|
+
.description('Herramienta de importación de cURL a config.json para Pruebas de Estrés')
|
|
10
|
+
.requiredOption('-i, --input <path>', 'Archivo de texto (.txt) que contiene el comando cURL crudo')
|
|
11
|
+
.requiredOption('-o, --output <path>', 'Ruta de destino para el config JSON de la prueba (ej: casos/az/nuevo-endpoint.json)')
|
|
12
|
+
.parse(process.argv);
|
|
13
|
+
|
|
14
|
+
const options = program.opts();
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(options.input)) {
|
|
17
|
+
console.error(`Error: El archivo de entrada "${options.input}" no existe.`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// 1. Leer RAW cURL
|
|
23
|
+
const curlCommand = fs.readFileSync(options.input, 'utf8').trim();
|
|
24
|
+
|
|
25
|
+
// 2. Usar curlconverter para extraer obj (Devuelve un string JSON equivalente en fetch-obj, o Node.js puro)
|
|
26
|
+
// curlconverter toJsonString genera un JSON representativo del request:
|
|
27
|
+
// { url, method, headers, data }
|
|
28
|
+
const parsedCurlStr = toJsonString(curlCommand);
|
|
29
|
+
if (!parsedCurlStr) {
|
|
30
|
+
throw new Error("El comando cURL no pudo ser parseado.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parsedCurl = JSON.parse(parsedCurlStr);
|
|
34
|
+
|
|
35
|
+
// 3. Generar la Plantilla de Cargo
|
|
36
|
+
const outputConfig = {
|
|
37
|
+
url: parsedCurl.url,
|
|
38
|
+
method: parsedCurl.method || 'GET',
|
|
39
|
+
startRate: 5,
|
|
40
|
+
targetRate: 50,
|
|
41
|
+
rampUpTime: 5,
|
|
42
|
+
duration: 10,
|
|
43
|
+
headers: parsedCurl.headers || {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 4. Analizar BODY puro vs Multipart (Form-Data)
|
|
47
|
+
// `curlconverter` lo expone en "data" si es string literal
|
|
48
|
+
// Y lo expone como objeto {data:{}, files:{}} si es Form-Data
|
|
49
|
+
|
|
50
|
+
// Utilidad para limpiar strings literales escapados que trae cURL a veces
|
|
51
|
+
function cleanVal(str) {
|
|
52
|
+
if(typeof str !== 'string') return str;
|
|
53
|
+
let clean = str.replace(/^\\"/, '"').replace(/\\"$/, '"');
|
|
54
|
+
clean = clean.replace(/^"|"$/g, '');
|
|
55
|
+
return clean.replace(/\\/g, '');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (parsedCurl.files && Object.keys(parsedCurl.files).length > 0) {
|
|
59
|
+
// 4A. Es una peticion con Archivo (Multipart)
|
|
60
|
+
const fileKeys = Object.keys(parsedCurl.files);
|
|
61
|
+
const firstFileKey = fileKeys[0];
|
|
62
|
+
|
|
63
|
+
outputConfig.filekey = firstFileKey;
|
|
64
|
+
let filePath = parsedCurl.files[firstFileKey];
|
|
65
|
+
|
|
66
|
+
// Cleanup de path local para Windows (A veces de vuelve /C:/Users)
|
|
67
|
+
if (filePath.match(/^\/[A-Za-z]:\//)) {
|
|
68
|
+
filePath = filePath.substring(1);
|
|
69
|
+
}
|
|
70
|
+
outputConfig.file = filePath;
|
|
71
|
+
|
|
72
|
+
// Tiene campos de texto extra acompañando al form?
|
|
73
|
+
if (parsedCurl.data && Object.keys(parsedCurl.data).length > 0) {
|
|
74
|
+
outputConfig.body = {};
|
|
75
|
+
for (const [key, val] of Object.entries(parsedCurl.data)) {
|
|
76
|
+
outputConfig.body[key] = cleanVal(val);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else if (parsedCurl.data) {
|
|
80
|
+
// 4B. Es una peticion normal con body JSON / texto
|
|
81
|
+
if(typeof parsedCurl.data === 'string') {
|
|
82
|
+
try {
|
|
83
|
+
outputConfig.body = JSON.parse(parsedCurl.data);
|
|
84
|
+
} catch(e) {
|
|
85
|
+
outputConfig.body = parsedCurl.data;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
outputConfig.body = parsedCurl.data; // Si ya es objeto
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 5. Asegurar estructura de carpeta y Escribir
|
|
93
|
+
const destPath = path.resolve(options.output);
|
|
94
|
+
const destDir = path.dirname(destPath);
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(destDir)){
|
|
97
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fs.writeFileSync(destPath, JSON.stringify(outputConfig, null, 2), 'utf8');
|
|
101
|
+
|
|
102
|
+
console.log(`✅ ¡Éxito!`);
|
|
103
|
+
console.log(`Tu comando cURL ha sido convertido a una configuración de carga.`);
|
|
104
|
+
console.log(`Archivo generado: ${destPath}`);
|
|
105
|
+
console.log(`\nPuedes ejecutar la prueba ahora usando:`);
|
|
106
|
+
console.log(`node index.js --config ${options.output}`);
|
|
107
|
+
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`💥 Error al procesar el cURL:`, err.message);
|
|
110
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const FormData = require('form-data');
|
|
5
|
+
const { program } = require('commander');
|
|
6
|
+
const cliProgress = require('cli-progress');
|
|
7
|
+
const { v4: uuidv4 } = require('uuid');
|
|
8
|
+
|
|
9
|
+
// -------------------------------------------------------------
|
|
10
|
+
// Configuración por Archivo JSON
|
|
11
|
+
// -------------------------------------------------------------
|
|
12
|
+
program
|
|
13
|
+
.version('2.0.0')
|
|
14
|
+
.description('Herramienta Avanzada HTTP/REST (Ramp-up y Datos Dinámicos)')
|
|
15
|
+
.requiredOption('-c, --config <path>', 'Ruta a un archivo JSON con toda la configuración (Obligatorio)')
|
|
16
|
+
.parse(process.argv);
|
|
17
|
+
|
|
18
|
+
const cliOpts = program.opts();
|
|
19
|
+
|
|
20
|
+
let options = {};
|
|
21
|
+
try {
|
|
22
|
+
const configData = JSON.parse(fs.readFileSync(cliOpts.config, 'utf8'));
|
|
23
|
+
options = { ...configData };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`Error crítico leyendo el archivo de configuración: ${error.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -------------------------------------------------------------
|
|
30
|
+
// Validaciones Básicas de la Configuración
|
|
31
|
+
// -------------------------------------------------------------
|
|
32
|
+
if (!options.url || !options.method) {
|
|
33
|
+
console.error('Error: Debes proporcionar "url" y "method" en el archivo de configuración JSON.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const method = options.method.toUpperCase();
|
|
38
|
+
if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
|
|
39
|
+
console.error(`Método HTTP no soportado: ${method}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Manejar tasas estáticas o Ramp-Up
|
|
44
|
+
const isRampUp = options.rampUpTime && options.targetRate;
|
|
45
|
+
const startRate = isRampUp ? (options.startRate || 1) : (options.rate || 10);
|
|
46
|
+
const targetRate = isRampUp ? options.targetRate : (options.rate || 10);
|
|
47
|
+
const rampUpTime = isRampUp ? options.rampUpTime : 0;
|
|
48
|
+
const duration = options.duration || 10;
|
|
49
|
+
|
|
50
|
+
// Función centralizada para calcular el rate en un segundo dado
|
|
51
|
+
function calculateRate(second) {
|
|
52
|
+
if (!isRampUp) return startRate;
|
|
53
|
+
if (second >= rampUpTime) return targetRate;
|
|
54
|
+
const rateDiff = targetRate - startRate;
|
|
55
|
+
const percentageDone = second / rampUpTime;
|
|
56
|
+
return Math.floor(startRate + (rateDiff * Math.min(percentageDone, 1)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let totalRequestsExpected = 0;
|
|
60
|
+
if (isRampUp) {
|
|
61
|
+
// Sumar iteración exacta segundo a segundo para reflejar la realidad del while loop
|
|
62
|
+
for (let s = 1; s <= duration; s++) {
|
|
63
|
+
totalRequestsExpected += calculateRate(s);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
totalRequestsExpected = startRate * duration;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let parsedHeaders = options.headers || {};
|
|
70
|
+
let templateBody = options.body ? JSON.stringify(options.body) : null;
|
|
71
|
+
if (options.file && !fs.existsSync(options.file)) {
|
|
72
|
+
console.error(`El archivo especificado no existe: ${options.file}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// -------------------------------------------------------------
|
|
77
|
+
// Utilidad: Reemplazar variables dinámicas
|
|
78
|
+
// -------------------------------------------------------------
|
|
79
|
+
function parseDynamicVariables(inputStr) {
|
|
80
|
+
if (!inputStr) return inputStr;
|
|
81
|
+
return inputStr
|
|
82
|
+
.replace(/\{\{timestamp\}\}/g, () => Date.now().toString())
|
|
83
|
+
.replace(/\{\{uuid\}\}/g, () => uuidv4())
|
|
84
|
+
.replace(/\{\{random_number\}\}/g, () => Math.floor(Math.random() * 10000).toString());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------
|
|
88
|
+
// Variables de Métricas
|
|
89
|
+
// -------------------------------------------------------------
|
|
90
|
+
const metrics = {
|
|
91
|
+
total: 0,
|
|
92
|
+
success: 0,
|
|
93
|
+
errors: 0,
|
|
94
|
+
latencies: [],
|
|
95
|
+
statusCodes: {},
|
|
96
|
+
startTime: null,
|
|
97
|
+
endTime: null
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let activeConnections = 0;
|
|
101
|
+
let maxConcurrentConnections = 0;
|
|
102
|
+
|
|
103
|
+
// Barra de progreso visual
|
|
104
|
+
const progressBar = new cliProgress.SingleBar({
|
|
105
|
+
format: 'Progreso | {bar} | {percentage}% || {value}/{total} Peticiones | Tasa: {throughput} req/s',
|
|
106
|
+
barCompleteChar: '\u2588',
|
|
107
|
+
barIncompleteChar: '\u2591',
|
|
108
|
+
hideCursor: true
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// -------------------------------------------------------------
|
|
112
|
+
// Función para crear la petición HTTP
|
|
113
|
+
// -------------------------------------------------------------
|
|
114
|
+
async function makeRequest() {
|
|
115
|
+
activeConnections++;
|
|
116
|
+
if (activeConnections > maxConcurrentConnections) {
|
|
117
|
+
maxConcurrentConnections = activeConnections;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const startTime = process.hrtime();
|
|
121
|
+
let reqConfig = {
|
|
122
|
+
method: method,
|
|
123
|
+
url: options.url,
|
|
124
|
+
headers: { ...parsedHeaders },
|
|
125
|
+
validateStatus: () => true, // Resolver siempre la promesa para capturar todos los códigos de estado
|
|
126
|
+
timeout: 60000, // Dar 60 segundos a QA para responder cada hilo individual
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Lógica de archivo (Multipart) vs JSON Body
|
|
130
|
+
let resolvedUrl = parseDynamicVariables(options.url);
|
|
131
|
+
reqConfig.url = resolvedUrl;
|
|
132
|
+
|
|
133
|
+
// Lógica de variables dinámicas en Content/Body y Headers
|
|
134
|
+
let finalHeaders = { ...parsedHeaders };
|
|
135
|
+
for (const [key, val] of Object.entries(finalHeaders)) {
|
|
136
|
+
if (typeof val === 'string') finalHeaders[key] = parseDynamicVariables(val);
|
|
137
|
+
}
|
|
138
|
+
reqConfig.headers = finalHeaders;
|
|
139
|
+
|
|
140
|
+
// Construir Payload (JSON puro vs Multipart)
|
|
141
|
+
if (options.file && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
142
|
+
const form = new FormData();
|
|
143
|
+
// 1. Adjuntar Archivo
|
|
144
|
+
form.append(options.filekey || 'file', fs.createReadStream(options.file));
|
|
145
|
+
|
|
146
|
+
// 2. Adjuntar otros campos del Body si existen
|
|
147
|
+
if (templateBody) {
|
|
148
|
+
const parsedDynamicString = parseDynamicVariables(templateBody);
|
|
149
|
+
const bodyObj = JSON.parse(parsedDynamicString);
|
|
150
|
+
for (const [key, value] of Object.entries(bodyObj)) {
|
|
151
|
+
// En multipart, todo lo extra va como string
|
|
152
|
+
form.append(key, String(value));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
reqConfig.data = form;
|
|
157
|
+
Object.assign(reqConfig.headers, form.getHeaders());
|
|
158
|
+
} else if (templateBody && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
159
|
+
const parsedDynamicString = parseDynamicVariables(templateBody);
|
|
160
|
+
reqConfig.data = JSON.parse(parsedDynamicString);
|
|
161
|
+
if (!reqConfig.headers['Content-Type']) {
|
|
162
|
+
reqConfig.headers['Content-Type'] = 'application/json';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await axios(reqConfig);
|
|
168
|
+
|
|
169
|
+
// Calcular tiempo
|
|
170
|
+
const endTime = process.hrtime(startTime);
|
|
171
|
+
const latencyMs = (endTime[0] * 1000) + (endTime[1] / 1000000);
|
|
172
|
+
|
|
173
|
+
metrics.latencies.push(latencyMs);
|
|
174
|
+
metrics.statusCodes[response.status] = (metrics.statusCodes[response.status] || 0) + 1;
|
|
175
|
+
|
|
176
|
+
// Consideramos éxito códigos 2xx y 3xx
|
|
177
|
+
if (response.status >= 200 && response.status < 400) {
|
|
178
|
+
metrics.success++;
|
|
179
|
+
} else {
|
|
180
|
+
metrics.errors++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// Errores de red (Timeout, conexión rechazada, dns)
|
|
185
|
+
metrics.errors++;
|
|
186
|
+
metrics.statusCodes['NETWORK_ERROR'] = (metrics.statusCodes['NETWORK_ERROR'] || 0) + 1;
|
|
187
|
+
} finally {
|
|
188
|
+
activeConnections--;
|
|
189
|
+
metrics.total++;
|
|
190
|
+
|
|
191
|
+
// Calcular throughput en vivo
|
|
192
|
+
const elapsedSecs = Math.max(0.1, (Date.now() - metrics.startTime) / 1000);
|
|
193
|
+
const currentThroughput = (metrics.total / elapsedSecs).toFixed(1);
|
|
194
|
+
|
|
195
|
+
progressBar.increment(1, { throughput: currentThroughput });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// -------------------------------------------------------------
|
|
200
|
+
// Motor de Ejecución (Ramp-up y Límite)
|
|
201
|
+
// -------------------------------------------------------------
|
|
202
|
+
async function runTest() {
|
|
203
|
+
// Guardar la cabecera de configuración para incluirla en reporte.txt
|
|
204
|
+
metrics.configHeader = [];
|
|
205
|
+
metrics.configHeader.push(`URL: ${options.url}`);
|
|
206
|
+
metrics.configHeader.push(`Método: ${method}`);
|
|
207
|
+
if (isRampUp) {
|
|
208
|
+
metrics.configHeader.push(`Ramp-up: De ${startRate} a ${targetRate} req/s en ${rampUpTime}s`);
|
|
209
|
+
metrics.configHeader.push(`Duración total: ${duration} segundos`);
|
|
210
|
+
metrics.configHeader.push(`Total Estimado: ~${totalRequestsExpected} peticiones`);
|
|
211
|
+
} else {
|
|
212
|
+
metrics.configHeader.push(`Peticiones constantes: ${startRate} req/s | Duración: ${duration}s`);
|
|
213
|
+
metrics.configHeader.push(`Total Estimado: ~${totalRequestsExpected} peticiones`);
|
|
214
|
+
}
|
|
215
|
+
if (options.file) metrics.configHeader.push(`Archivo adjunto: ${options.file}`);
|
|
216
|
+
|
|
217
|
+
console.log(`\nIniciando Prueba de Estrés HTTP 🚀`);
|
|
218
|
+
console.log(`URL: ${options.url}`);
|
|
219
|
+
console.log(`Método: ${method}`);
|
|
220
|
+
if (isRampUp) {
|
|
221
|
+
console.log(`Ramp-up: De ${startRate} a ${targetRate} req/s en ${rampUpTime}s`);
|
|
222
|
+
console.log(`Mantenido en ${targetRate} req/s hasta alcanzar los ${duration}s de prueba.`);
|
|
223
|
+
} else {
|
|
224
|
+
console.log(`Peticiones constantes: ${startRate} peticiones/s | Duración: ${duration}s`);
|
|
225
|
+
}
|
|
226
|
+
if (options.file) console.log(`Cargando archivo: ${options.file}`);
|
|
227
|
+
console.log(`Total Estimado: ~${totalRequestsExpected} peticiones\n`);
|
|
228
|
+
|
|
229
|
+
metrics.startTime = Date.now();
|
|
230
|
+
progressBar.start(totalRequestsExpected, 0, { throughput: 0 });
|
|
231
|
+
|
|
232
|
+
let currentSecond = 0;
|
|
233
|
+
let sentRequests = 0;
|
|
234
|
+
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
const interval = setInterval(async () => {
|
|
237
|
+
currentSecond++;
|
|
238
|
+
const currentRate = calculateRate(currentSecond);
|
|
239
|
+
|
|
240
|
+
const requestsForThisSecond = [];
|
|
241
|
+
for (let i = 0; i < currentRate; i++) {
|
|
242
|
+
if (sentRequests >= totalRequestsExpected) break;
|
|
243
|
+
requestsForThisSecond.push(makeRequest());
|
|
244
|
+
sentRequests++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (currentSecond >= duration) {
|
|
248
|
+
clearInterval(interval);
|
|
249
|
+
|
|
250
|
+
let waitTime = 0;
|
|
251
|
+
const checkDone = setInterval(() => {
|
|
252
|
+
waitTime++;
|
|
253
|
+
if (activeConnections === 0 || waitTime > 180) {
|
|
254
|
+
// 90 segundos máximos de gracia esperando que Node reciba respuesta de QA (bottleneck)
|
|
255
|
+
clearInterval(checkDone);
|
|
256
|
+
metrics.endTime = Date.now();
|
|
257
|
+
progressBar.stop();
|
|
258
|
+
resolve();
|
|
259
|
+
}
|
|
260
|
+
}, 500);
|
|
261
|
+
}
|
|
262
|
+
}, 1000);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// -------------------------------------------------------------
|
|
267
|
+
// Reporte de Métricas Final
|
|
268
|
+
// -------------------------------------------------------------
|
|
269
|
+
function analyzeMetrics() {
|
|
270
|
+
let reportText = "";
|
|
271
|
+
const logAndSave = (text) => {
|
|
272
|
+
console.log(text);
|
|
273
|
+
reportText += text + "\n";
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Encabezado de configuración al inicio del reporte
|
|
277
|
+
reportText += `CONFIGURACIÓN DE LA PRUEBA\n`;
|
|
278
|
+
reportText += `==========================================\n`;
|
|
279
|
+
if (metrics.configHeader) {
|
|
280
|
+
metrics.configHeader.forEach(line => { reportText += line + "\n"; });
|
|
281
|
+
}
|
|
282
|
+
reportText += `\n`;
|
|
283
|
+
|
|
284
|
+
logAndSave(`\n===========================================`);
|
|
285
|
+
logAndSave(`📊 RESUMEN DE LA PRUEBA`);
|
|
286
|
+
logAndSave(`===========================================`);
|
|
287
|
+
|
|
288
|
+
if (metrics.total === 0) {
|
|
289
|
+
logAndSave("No se completó ninguna petición.");
|
|
290
|
+
fs.writeFileSync('reporte.txt', reportText, 'utf8');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Ordenar latencias numéricamente
|
|
295
|
+
metrics.latencies.sort((a, b) => a - b);
|
|
296
|
+
const min = metrics.latencies[0] || 0;
|
|
297
|
+
const max = metrics.latencies[metrics.latencies.length - 1] || 0;
|
|
298
|
+
const sum = metrics.latencies.reduce((a, b) => a + b, 0);
|
|
299
|
+
const avg = metrics.latencies.length > 0 ? sum / metrics.latencies.length : 0;
|
|
300
|
+
|
|
301
|
+
// Percentil 95
|
|
302
|
+
const p95Index = Math.floor(metrics.latencies.length * 0.95);
|
|
303
|
+
const p95 = metrics.latencies[p95Index] || 0;
|
|
304
|
+
|
|
305
|
+
// Throughput Real
|
|
306
|
+
const totalTestDurationSecs = Math.max(0.1, (metrics.endTime - metrics.startTime) / 1000);
|
|
307
|
+
const avgThroughput = (metrics.total / totalTestDurationSecs).toFixed(2);
|
|
308
|
+
|
|
309
|
+
// Separar los distintos tipos de éxito/fallo
|
|
310
|
+
let count2xx = 0, count3xx = 0, count4xx = 0, count5xx = 0;
|
|
311
|
+
for (const code of Object.keys(metrics.statusCodes)) {
|
|
312
|
+
const numericCode = parseInt(code, 10);
|
|
313
|
+
if (numericCode >= 200 && numericCode < 300) count2xx += metrics.statusCodes[code];
|
|
314
|
+
else if (numericCode >= 300 && numericCode < 400) count3xx += metrics.statusCodes[code];
|
|
315
|
+
else if (numericCode >= 400 && numericCode < 500) count4xx += metrics.statusCodes[code];
|
|
316
|
+
else if (numericCode >= 500) count5xx += metrics.statusCodes[code];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
logAndSave(`🎯 Peticiones Totales : ${metrics.total}`);
|
|
320
|
+
logAndSave(`⚡ Conexiones Máximas : ${maxConcurrentConnections}`);
|
|
321
|
+
logAndSave(`🚀 Rendimiento / Rate : ${avgThroughput} Req/s`);
|
|
322
|
+
logAndSave(`\n📊 DESGLOSE DE RESULTADOS:`);
|
|
323
|
+
logAndSave(`✅ Exitosas (2xx) : ${count2xx}`);
|
|
324
|
+
if (count3xx > 0) logAndSave(`🔀 Redirecciones (3xx) : ${count3xx}`);
|
|
325
|
+
logAndSave(`⚠️ Errores Cliente (4xx): ${count4xx} <- (Bloqueos, No Autorizado, Bad Request)`);
|
|
326
|
+
logAndSave(`❌ Errores Server (5xx): ${count5xx + (metrics.statusCodes['NETWORK_ERROR'] || 0)} <- (Caídas del Servidor, Timeout)`);
|
|
327
|
+
|
|
328
|
+
logAndSave(`\n⏱️ TIEMPOS DE RESPUESTA (Latencia en ms):`);
|
|
329
|
+
logAndSave(` Mínimo : ${min.toFixed(2)} ms`);
|
|
330
|
+
logAndSave(` Máximo : ${max.toFixed(2)} ms`);
|
|
331
|
+
logAndSave(` Promedio: ${avg.toFixed(2)} ms`);
|
|
332
|
+
logAndSave(` P95 : ${p95.toFixed(2)} ms`);
|
|
333
|
+
|
|
334
|
+
logAndSave(`\n🏷️ CÓDIGOS DE ESTADO HTTP:`);
|
|
335
|
+
for (const [code, count] of Object.entries(metrics.statusCodes)) {
|
|
336
|
+
logAndSave(` [${code}] -> ${count} veces`);
|
|
337
|
+
}
|
|
338
|
+
logAndSave(`===========================================\n`);
|
|
339
|
+
|
|
340
|
+
// Escribir el reporte completo en el archivo
|
|
341
|
+
try {
|
|
342
|
+
fs.writeFileSync('reporte.txt', reportText, 'utf8');
|
|
343
|
+
console.log(`\n📄 Reporte guardado exitosamente en: reporte.txt`);
|
|
344
|
+
} catch(e) {
|
|
345
|
+
console.error(`\nNo se pudo guardar el reporte.txt: ${e.message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Iniciar
|
|
350
|
+
runTest().then(() => {
|
|
351
|
+
analyzeMetrics();
|
|
352
|
+
}).catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prix-r9",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Herramienta CLI ligera para pruebas de estrés de APIs REST con ramp-up progresivo y variables dinámicas.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"prix-r9": "index.js",
|
|
8
|
+
"prix-r9-curl": "import-curl.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"import-curl.js",
|
|
13
|
+
"promptInforme.txt",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"stress",
|
|
21
|
+
"load",
|
|
22
|
+
"api",
|
|
23
|
+
"qa",
|
|
24
|
+
"testing",
|
|
25
|
+
"http",
|
|
26
|
+
"ramp-up",
|
|
27
|
+
"benchmark",
|
|
28
|
+
"performance"
|
|
29
|
+
],
|
|
30
|
+
"author": "QA Team",
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"type": "commonjs",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=16.0.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"axios": "^1.13.5",
|
|
39
|
+
"cli-progress": "^3.12.0",
|
|
40
|
+
"commander": "^14.0.3",
|
|
41
|
+
"curlconverter": "^4.12.0",
|
|
42
|
+
"form-data": "^4.0.5",
|
|
43
|
+
"uuid": "^13.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Actúa como un Ingeniero QA Senior con experiencia en pruebas de rendimiento y carga de APIs REST.
|
|
2
|
+
A continuación te proporciono los resultados de MÚLTIPLES ESCENARIOS de pruebas de estrés HTTP
|
|
3
|
+
realizadas contra endpoint(s) de un sistema financiero en ambiente QA.
|
|
4
|
+
Cada escenario tiene una configuración distinta (diferente Ramp-Up, carga, duración, etc.)
|
|
5
|
+
y fue ejecutado de forma independiente para comparar el comportamiento del servidor bajo distintos niveles de estrés.
|
|
6
|
+
|
|
7
|
+
Necesito que redactes un INFORME TÉCNICO PROFESIONAL en español con las siguientes secciones:
|
|
8
|
+
|
|
9
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
10
|
+
1. RESUMEN EJECUTIVO
|
|
11
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
12
|
+
Describe en 3-4 oraciones qué se probó, cuántos escenarios hubo y cuál fue la conclusión general.
|
|
13
|
+
Redacta sin tecnicismos, orientado a gerencia.
|
|
14
|
+
|
|
15
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
16
|
+
2. TABLA COMPARATIVA DE ESCENARIOS
|
|
17
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
18
|
+
Crea una tabla comparativa con una fila por escenario y las siguientes columnas:
|
|
19
|
+
- Escenario (nombre o número)
|
|
20
|
+
- Carga configurada (startRate → targetRate)
|
|
21
|
+
- Peticiones totales
|
|
22
|
+
- Exitosas (2xx)
|
|
23
|
+
- Errores cliente (4xx)
|
|
24
|
+
- Errores servidor (5xx)
|
|
25
|
+
- Rendimiento (Req/s)
|
|
26
|
+
- Latencia Promedio (ms)
|
|
27
|
+
- P95 (ms)
|
|
28
|
+
- Conexiones Máximas
|
|
29
|
+
- Resultado general (✅ Estable / ⚠️ Degradado / ❌ Crítico)
|
|
30
|
+
|
|
31
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
32
|
+
3. ANÁLISIS POR ESCENARIO
|
|
33
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
+
Para cada escenario, analiza en detalle:
|
|
35
|
+
- Qué nivel de carga se aplicó y por qué el Ramp-Up gradual es importante
|
|
36
|
+
- Comportamiento del servidor: ¿respondió bien, se degradó, o se protegió?
|
|
37
|
+
- Interpretación de los errores 4xx vs 5xx
|
|
38
|
+
- Qué significa el P95 en ese contexto y por qué difiere del Promedio
|
|
39
|
+
- Qué indica el número de Conexiones Máximas sobre la concurrencia soportada
|
|
40
|
+
|
|
41
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
4. DIAGNÓSTICO GENERAL DEL SERVIDOR
|
|
43
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
Basándote en el patrón de todos los escenarios, diagnostica:
|
|
45
|
+
- ¿En qué nivel de carga el servidor empieza a degradarse?
|
|
46
|
+
- ¿Los errores 401 son por autenticación inválida o por Rate Limiting / WAF?
|
|
47
|
+
- ¿Hay señales de cuello de botella en base de datos, memoria o red?
|
|
48
|
+
- ¿El servidor se recupera o se queda degradado una vez superado el umbral?
|
|
49
|
+
|
|
50
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
5. CONCLUSIONES Y RECOMENDACIONES
|
|
52
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
53
|
+
- ¿Cuál es la carga máxima segura que soporta el endpoint actualmente?
|
|
54
|
+
- Acciones concretas para el equipo de desarrollo e infraestructura
|
|
55
|
+
- ¿Qué escenario debería usarse como prueba de regresión en futuros sprints?
|
|
56
|
+
|
|
57
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
58
|
+
6. SEMÁFORO DE SALUD DEL SISTEMA
|
|
59
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
60
|
+
Genera una tabla con los indicadores clave clasificados en:
|
|
61
|
+
🟢 Verde = dentro de límites aceptables
|
|
62
|
+
🟡 Amarillo = requiere atención
|
|
63
|
+
🔴 Rojo = crítico, acción inmediata requerida
|
|
64
|
+
|
|
65
|
+
El tono debe ser formal y técnico, adecuado para presentar tanto al equipo de desarrollo como a gerencia de TI.
|
|
66
|
+
|
|
67
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
68
|
+
RESULTADOS DE LOS ESCENARIOS:
|
|
69
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
70
|
+
|
|
71
|
+
[ESCENARIO 1]
|
|
72
|
+
[PEGA AQUÍ EL CONTENIDO DEL reporte.txt DEL ESCENARIO 1]
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
[ESCENARIO 2]
|
|
77
|
+
[PEGA AQUÍ EL CONTENIDO DEL reporte.txt DEL ESCENARIO 2]
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
[ESCENARIO 3 - Agrega tantos como necesites con el mismo formato]
|
|
82
|
+
[PEGA AQUÍ EL CONTENIDO DEL reporte.txt DEL ESCENARIO 3]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
88
|
+
INFORMACIÓN ADICIONAL DEL SISTEMA:
|
|
89
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
90
|
+
Endpoint evaluado: [Nombre del endpoint, ej: GetReportAlliedBusinessByCompany]
|
|
91
|
+
Sistema: [Nombre del sistema, ej: MDL37 - Reportes de Comercios Aliados]
|
|
92
|
+
Ambiente: QA
|
|
93
|
+
Fecha de prueba: [Fecha]
|