Testing negativo en Postman: rompiendo el login de Serenity Demo a propósito
4 escenarios que rompen el login a propósito. Credenciales incorrectas, sin CSRF, sin auth, campos vacíos. Un 500 que no debería estar ahí y una cookie trampa.
Contexto
En el post anterior armé 11 assertions sobre el flujo exitoso: login, cookies, Customer List, estructura JSON, TotalCount. Todo verde.
Pero un flujo que solo valida el camino feliz no testea casi nada. ¿Qué pasa si mando credenciales incorrectas? ¿Si pido datos sin estar autenticado? ¿Si salteo el CSRF? ¿Si mando campos vacíos?
Este post rompe cosas a propósito.
Los 4 escenarios
Armé 4 escenarios negativos, cada uno ataca una parte distinta del flujo:
- Login con credenciales incorrectas — password mal, usuario mal, ambos mal
- Customer List sin autenticación — pedir la grilla sin loguearse
- Login sin CSRF token — saltear el GET Login Page e ir directo al POST
- Login con campos vacíos — username y password como strings vacíos
Escenario 1: Login con credenciales incorrectas
Las assertions
pm.test("Status code is 400", function () {
pm.response.to.have.status(400);
});
pm.test("Response has AuthenticationError", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.Error).to.not.be.undefined;
pm.expect(jsonData.Error.Code).to.eql("AuthenticationError");
});
pm.test("Error message is generic (no field hint)", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.Error.Message).to.eql("Invalid username or password!");
});
Las mismas 3 assertions para las 3 variantes.

Por qué estas 3:
La primera verifica que el servidor rechace la request con 400 Bad Request. No 200, no 500, no redirect. Un login fallido tiene que devolver un error de cliente.
La segunda entra en el body y valida que el JSON tenga la estructura de error esperada: un objeto Error con Code: "AuthenticationError". Si algún día el backend cambia la estructura de respuesta o devuelve un error distinto, este test lo detecta.
La tercera verifica que el mensaje sea genérico: "Invalid username or password!". No dice "Invalid password" ni "User not found". Eso importa por seguridad: si el mensaje revelara cuál campo falló, un atacante podría enumerar usuarios válidos. Este test verifica que eso no pase.
Dupliqué el POST - Login tres veces:
POST - Login (user correcto, pass mal)→{"Username":"admin","Password":"abcd"}POST - Login (user mal, pass correcta)→{"Username":"abcd","Password":"serenity"}POST - Login (user y pass incorrectos)→{"Username":"abcd","Password":"abcd"}
Las tres devolvieron exactamente lo mismo:
- Status: 400 Bad Request
- Body:
{
"Error": {
"Code": "AuthenticationError",
"Message": "Invalid username or password!"
}
}

No importa si el usuario está mal, la password está mal, o ambos. El servidor devuelve el mismo error, el mismo status, el mismo mensaje. No revela cuál campo falló.
Eso es buena práctica de seguridad: un atacante no puede distinguir si acertó el usuario o no.
La trampa de la cookie jar
La primera vez que corrí las 3 requests negativas en el Collection Runner, copié las assertions del POST - Login exitoso: status 200, .AspNetAuth cookie exists, CSRF-TOKEN cookie still present.

Resultado: "Status code is 200" falló (el servidor devolvió 400, esperado). Pero .AspNetAuth cookie exists after login pasó en verde.

¿Un login fallido crea la cookie de autenticación? No. Lo que pasaba era otra cosa.
El Collection Runner ejecuta las requests en orden. El POST - Login exitoso (credenciales correctas) LO CORRÍ antes que las variantes negativas. Ese login guarda .AspNetAuth en la cookie jar de Postman. Cuando después corren las requests con credenciales incorrectas, la cookie sigue ahí. El test busca "¿existe esta cookie?", la encuentra, y dice PASS.
Un PASS falso. La cookie no la creó el login fallido. La arrastró el login exitoso.
Cómo lo confirmé
Limpié la cookie jar de Postman (Cookies → demo.serenity.is → borrar todo) y corrí el Runner de nuevo.
Resultado: .AspNetAuth cookie exists after login ahora FAIL en las 3 variantes negativas. expected undefined not to be undefined.

Ahí estaba la prueba. Sin la cookie del login exitoso, el test falla. El login fallido nunca creó la cookie.
Limpieza
Eliminé las assertions del flujo positivo (status 200, .AspNetAuth, CSRF-TOKEN) de las 3 requests negativas. Dejé solo las 3 assertions que validan lo que realmente corresponde a un login fallido.

La lección: el Runner comparte estado entre requests. Si no entendés eso, vas a tener assertions que pasan por las razones equivocadas. Un PASS verde no siempre significa que la funcionalidad está bien. A veces significa que tu test está mal.
Escenario 2: Customer List sin autenticación
Limpié las cookies y ejecuté directamente el POST - Customer List, sin correr el POST-login antes.
Resultado:
- Status: 400 Bad Request
- Body: vacío (content-length: 0)
- Cookies: ninguna
- Content-Type: ausente

Nada. El servidor no devuelve JSON, no devuelve error, no devuelve mensaje. Body vacío, 0 bytes.
A nivel de seguridad esto es bueno: sin autenticación, el servidor no te confirma que el endpoint existe. No filtra información.
Las 5 assertions del flujo positivo fallan todas:
- Status code is 200 → FAIL (got 400)
- Response is JSON → FAIL (no Content-Type header)
- Response has Entities array → FAIL (JSONError: No data, empty input)
- TotalCount is 91 → FAIL (JSONError: No data, empty input)
- Each entity has CustomerID and CompanyName → FAIL (JSONError: No data, empty input)
Qué está validando cada una al fallar:
La de status confirma que sin auth el servidor no responde con 200. La de JSON confirma que no devuelve datos en formato legible. Las de Entities, TotalCount y campos obligatorios confirman que no se filtran datos de la grilla. Cinco formas de decir lo mismo: sin autenticación, no hay datos. Ni parciales, ni accidentales, ni nada.
No escribí assertions específicas para este escenario (tipo "status code is 400" y "body is empty"). Ejecuté las del flujo positivo a propósito para mostrar cómo reacciona la misma colección ante un contexto diferente. Los 5 FAIL rojos son el resultado correcto.
Escenario 3: Login sin CSRF token
Limpié cookies y ejecuté directamente el POST - Login (con credenciales correctas) sin pasar por GET - Login Page.
Sin el GET previo, no hay cookie CSRF-TOKEN. El header x-csrf-token del pre-request script intenta leer una variable que no existe.
Resultado:
- Status: 400 Bad Request
- Body: vacío (content-length: 0)
- Cookies: ninguna

Mismo patrón que Customer List sin auth: 400, body vacío, silencio total.
Las 3 assertions del POST - Login (status 200, .AspNetAuth cookie, CSRF-TOKEN cookie) fallan las 3. El status no es 200. No se crea cookie de autenticación. No hay CSRF-TOKEN porque nunca se hizo el GET previo que la genera.
La protección anti-CSRF funciona. El servidor no acepta el POST de login si no tiene el token CSRF que entregó en el GET previo. Esto conecta directamente con lo que resolví en el post del CSRF dinámico: el GET - Login Page no es decorativo, es obligatorio.
Escenario 4: Login con campos vacíos
Creé una request nueva: POST - Login (user y pass vacíos) con body {"Username":"","Password":""}.
Esperaba un 400 similar al de credenciales incorrectas. Quizás un mensaje tipo "Username is required".
Lo que recibí:
- Status: 500 Internal Server Error
- Body:
{
"Error": {
"Code": "Exception",
"Message": "An error occurred while processing your request."
}
}

No un 400. Un 500. No un AuthenticationError. Un Exception.
El servidor no validó que los campos estuvieran vacíos antes de procesarlos. Los mandó directo a la lógica de autenticación, esa lógica no supo qué hacer con strings vacíos, y explotó.
¿Es un bug?
No tengo acceso al equipo de desarrollo de Serenity Demo. No puedo confirmar si esto es intencional.
Pero un 500 Internal Server Error para un input vacío no parece comportamiento esperado. El patrón del resto de la API es claro: credenciales incorrectas → 400 con AuthenticationError. Campos vacíos deberían seguir el mismo patrón: 400 con un mensaje de validación. No un crash del servidor.
Las assertions que escribí documentan el comportamiento actual, no el ideal:
pm.test("Status code is 500", function () {
pm.response.to.have.status(500);
});
pm.test("Response has Exception error", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.Error).to.not.be.undefined;
pm.expect(jsonData.Error.Code).to.eql("Exception");
});
pm.test("Error message is generic server error", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.Error.Message).to.eql("An error occurred while processing your request.");
});
Por qué estas 3 assertions:
La primera valida que el status sea 500, no 400. Es una diferencia importante: 400 es "tu request está mal" (error de cliente), 500 es "yo me rompí" (error de servidor). Documentar que el servidor devuelve 500 para campos vacíos es documentar que el backend no maneja bien este caso.
La segunda verifica que el código de error sea "Exception" (error genérico del servidor), no "AuthenticationError" (error controlado de autenticación). Son respuestas estructuralmente diferentes aunque usen el mismo formato JSON. El servidor distingue entre "credenciales inválidas" y "esto no debería haber pasado".
La tercera confirma que el mensaje es el genérico de crash del servidor. No es un mensaje de validación tipo "Username is required". Es un "algo se rompió". Documentar esto es evidencia de que el input vacío no pasa por validación antes de llegar a la lógica de negocio.
Los tests pasan. El Runner muestra verde. Pero el 500 no debería ser la respuesta correcta a un input vacío.
Acá hay una distinción que importa: las assertions documentan qué pasa, no definen qué es correcto. Un test verde no significa que la funcionalidad está bien. Significa que el test coincide con la realidad. Y la realidad puede tener bugs.
Comportamiento del servidor: resumen
| Escenario | Status | Body | Cookies |
|---|---|---|---|
| Credenciales incorrectas (3 variantes) | 400 | JSON: AuthenticationError | No crea .AspNetAuth |
| Customer List sin autenticación | 400 | Vacío (0 bytes) | Ninguna |
| Login sin CSRF token | 400 | Vacío (0 bytes) | Ninguna |
| Campos vacíos | 500 | JSON: Exception | No crea .AspNetAuth |
Los 4 escenarios rechazan la request. Pero el servidor se comporta diferente en cada caso:
- Credenciales incorrectas: te dice qué falló (AuthenticationError con mensaje claro)
- Sin autenticación / sin CSRF: te da body vacío (ni siquiera confirma que el endpoint existe)
- Campos vacíos: crashea (500 con Exception genérica)
Tres niveles de respuesta para tres tipos de fallo. El testing negativo los expone.
Estado actual de la colección
La colección ahora tiene dos flujos separados en el Runner:
Runner positivo (del post anterior):


| Request | Tests | Qué valida |
|---|---|---|
| GET - Login Page | 3 | Status 200, HTML, cookie CSRF |
| POST - Login | 3 | Status 200, cookie .AspNetAuth, cookie CSRF |
| POST - Customer List | 5 | Status 200, JSON, Entities, TotalCount, campos obligatorios |
| Total | 11 |
Runner negativo (este post):


| Request | Tests | Qué valida |
|---|---|---|
| GET - Login Page | 3 | Status 200, HTML, cookie CSRF |
| POST - Login (user correcto, pass mal) | 3 | Status 400, AuthenticationError, mensaje genérico |
| POST - Login (user mal, pass correcta) | 3 | Status 400, AuthenticationError, mensaje genérico |
| POST - Login (user y pass incorrectos) | 3 | Status 400, AuthenticationError, mensaje genérico |
| POST - Login (user y pass vacíos) | 3 | Status 500, Exception, server error |
| Total | 15 |
Los escenarios de Customer List sin autenticación y Login sin CSRF fueron pruebas manuales individuales (fuera del Runner). Sus resultados están documentados arriba.
De 11 assertions en el post anterior a 26 en total (11 positivas + 15 negativas). Y esas 26 cubren el flujo exitoso completo más 4 escenarios negativos.
Lo que expone el testing negativo
La cookie jar del Runner es estado compartido. Las cookies que crea una request persisten para las siguientes. Si no entendés eso, tus assertions pueden pasar por razones equivocadas. Limpiar la cookie jar y volver a correr es la forma de confirmar si un PASS es real o heredado.
Un test verde no significa que la funcionalidad está bien. Significa que el test coincide con el comportamiento actual. Si el comportamiento tiene un bug (como el 500 en campos vacíos), el test verde solo documenta el bug. El criterio de QA es lo que distingue entre "el test pasa" y "esto está bien".
El servidor habla diferente según el tipo de fallo. Credenciales incorrectas → error explicativo. Sin auth → silencio total. Campos vacíos → crash. Un API bien diseñada debería tener un patrón consistente. Las inconsistencias se descubren rompiendo cosas a propósito.
Próximo paso
Hasta ahora toda la colección consulta datos. Las requests son GET y POST de lectura: login, listar clientes.
El siguiente paso es testear operaciones que modifican datos: crear un cliente desde Postman, verificar que exista en la grilla, eliminarlo y confirmar que ya no aparece. Eso es CRUD real, no solo consultas.
También quiero resolver un problema que encontré: después de limpiar cookies, la primera corrida del Runner positivo falla en Customer List (400, body vacío), aunque Login pasa con 200. Si corro el Runner de nuevo, funciona. Algo pasa con el estado entre el primer login y la primera consulta. Tengo que investigar qué es.