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.

Postman Collection Runner showing all 15 tests passed across GET Login Page and four negative login scenarios including incorrect credentials and empty fields
Resultado final: 15 assertions, 15 verdes. GET Login Page + 4 escenarios negativos validados.

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:

  1. Login con credenciales incorrectas — password mal, usuario mal, ambos mal
  2. Customer List sin autenticación — pedir la grilla sin loguearse
  3. Login sin CSRF token — saltear el GET Login Page e ir directo al POST
  4. 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.

Postman Scripts tab for POST Login with incorrect password showing three post-response assertions for status 400, AuthenticationError code and generic error message
Las 3 assertions del login con credenciales incorrectas: status 400, código de error y mensaje genérico.

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!"
    }
}
Postman Collection Runner with 12 passed tests showing request headers for POST Login with incorrect credentials including x-csrf-token and cookies sent with 400 Bad Request response
Los headers de la request con credenciales incorrectas: el CSRF token viaja en el header, el servidor responde 400.

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 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.

Postman Scripts tab for POST Login showing three post-response assertions for status 200, AspNetAuth cookie exists and CSRF-TOKEN cookie still present
Las assertions del login exitoso: status 200, cookie de autenticación y CSRF-TOKEN presentes.

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

Corrida con assertions de login exitoso
Assertion sobre .AspNetAuth debería fallar.

¿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.

Postman Collection Runner after clearing cookies showing FAIL on AspNetAuth cookie assertion with expected undefined not to be undefined error and FAIL on status 200 across three negative login requests
Después de limpiar la cookie jar: .AspNetAuth FAIL en las 3 variantes. La cookie no la creó el login fallido — la arrastraba el login exitoso.

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.

Postman Collection Runner with all 12 tests passed showing clean negative test assertions and request headers with x-csrf-token cookie and 400 Bad Request status for incorrect credentials
Runner limpio: solo las 3 assertions correctas por request, 12/12 verde.

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
Postman Collection Runner showing GET Login Page passed and POST Customer List failed with 400 Bad Request status, empty body with content-length 0, and all 5 assertions failed including status, JSON, Entities, TotalCount and entity field
Customer List sin login previo: 400, body vacío, 5 assertions fallidas. Sin autenticación no hay datos.

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
Postman Collection Runner showing POST Login with 400 Bad Request and all 3 assertions failed including status 200, AspNetAuth cookie and CSRF-TOKEN cookie with response body empty
Login sin pasar por GET Login Page: 400, body vacío, sin cookies. La protección anti-CSRF funciona.

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."
    }
}
Postman Collection Runner showing GET Login Page passed and POST Login with empty credentials returning 500 Internal Server Error with old assertions failing on status 400 and AuthenticationError while new assertions passing on status 500 and Exception error
Campos vacíos: 500 Internal Server Error. Las assertions de 400 fallan, las de 500 pasan. El servidor no valida inputs vacíos.

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):

Postman Collection Runner configuration showing Run Sequence with GET Login Page, POST Login and POST Customer List selected while four negative login variants are deselected
Configuración del Runner positivo: solo GET Login Page, POST Login y POST Customer List seleccionados.
Postman Collection Runner with all 11 tests passed showing GET Login Page, POST Login and POST Customer List results with Customer List JSON response displaying Entities array with customer records including CustomerID, CompanyName, ContactName and other fields
Runner positivo: 11/11 verde. La response de Customer List muestra los 91 registros con todos sus campos.
RequestTestsQué valida
GET - Login Page3Status 200, HTML, cookie CSRF
POST - Login3Status 200, cookie .AspNetAuth, cookie CSRF
POST - Customer List5Status 200, JSON, Entities, TotalCount, campos obligatorios
Total11

Runner negativo (este post):

Postman Collection Runner configuration showing Run Sequence with GET Login Page and four negative login variants selected while POST Login and POST Customer List are deselected
Configuración del Runner negativo: GET Login Page + 4 variantes de login negativo. Sin POST Login exitoso ni Customer List.
Postman Collection Runner with all 15 tests passed showing GET Login Page with 3 passes, three incorrect credential variants each with 3 passes on status 400 and AuthenticationError, and empty credentials variant with 3 passes on status 500 and Exception error, with request headers panel showing cookies and x-csrf-token
Runner negativo completo: 15/15 verde. Tres variantes de credenciales → 400. Campos vacíos → 500. Cada escenario con sus assertions específicas.
RequestTestsQué valida
GET - Login Page3Status 200, HTML, cookie CSRF
POST - Login (user correcto, pass mal)3Status 400, AuthenticationError, mensaje genérico
POST - Login (user mal, pass correcta)3Status 400, AuthenticationError, mensaje genérico
POST - Login (user y pass incorrectos)3Status 400, AuthenticationError, mensaje genérico
POST - Login (user y pass vacíos)3Status 500, Exception, server error
Total15

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.