fix(auth): el login no responde 404, y el pacto se regenera contra el sobre real - #16
Merged
Merged
Conversation
… sobre real
Cierra el único check rojo que quedaba en main (`Ums.ContractTest` 37/38, rojo desde
la resincronización). Eran dos fallos y solo uno era de contrato.
## El 404 era un oráculo de enumeración
`MapAuthError` devolvía 404 para AUTH_002 (tenant inexistente) y AUTH_004 (usuario
inexistente), y 401 para AUTH_006 (credenciales inválidas). Los mensajes de AUTH_004 y
AUTH_006 son idénticos palabra por palabra —alguien los escribió así a propósito, para
que no se distinga «esa cuenta no existe» de «te equivocaste»— y el estado deshacía ese
cuidado: bastaba mirar 404 contra 401 para saberlo, sin leer el cuerpo. Una lista de
correos contra POST /api/v1/auth/login separa las cuentas reales de las inventadas a
petición por candidato.
Es la misma conclusión que G-053 ya había aplicado en `/client/authenticate`, el
endpoint anónimo de máquina, donde AUTH_002 y AUTH_003 se colapsaron a 401 con mensaje
genérico. El endpoint de portal nunca recibió ese tratamiento.
AUTH_004 pasa a 401, indistinguible de AUTH_006. AUTH_002 pasa a 400 —y no a 401—
porque aquí el CUERPO ya revela el tenant a propósito («Verifique el código del
tenant»): colapsar el estado no ocultaría nada que el mensaje siga diciendo. Esa
asimetría es una decisión de producto anterior a este cambio; queda señalada en el ADR
para que no se lea como olvido.
Registrado como ADR-0165, porque 401 donde el instinto REST pide 404 es justo lo que
alguien «arregla» de vuelta si el motivo no está escrito.
## El pacto verificaba un cuerpo que la API dejó de enviar
Declaraba `{ status, title }` —ProblemDetails, la forma por defecto de ASP.NET— y esta
API responde `LoginErrorResponse`: `{ code, message, supportReferenceId }`. El
`supportReferenceId` es deliberado y es lo único que permite cruzar la queja de una
persona con la traza del servidor, así que el contrato se corrige hacia la API.
De paso, la aserción de `Content-Type` usaba `Match.Type("application/problem+json")`:
un matcher de tipo sobre una cadena casa con CUALQUIER cadena, así que esa línea daba
OK contra un content type que la API ya no envía. La comprobación existía y no
comprobaba nada — la misma forma que las claves de GraphQL de TD-004. Ahora se afirma
por valor exacto.
El front no cambia: `auth.service.ts` elige el mensaje por el `code` del cuerpo, nunca
por el estado.
Verificado: contrato 38/38, Domain 930/930, Application 934/934, Integración 314/316
(2 saltados), `dotnet build` 0 errores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cierra el único check rojo que quedaba en
main—Ums.ContractTest37/38, rojo desde la resincronización (c011e42a). Eran dos fallos y solo uno era de contrato.1. El 404 era un oráculo de enumeración
MapAuthErrordevolvía:AUTH_002AUTH_004AUTH_006Los mensajes de
AUTH_004yAUTH_006son idénticos palabra por palabra — «No pudimos iniciar sesión. Verifique sus credenciales.» Alguien los escribió así a propósito, para que quien intenta entrar no distinga «esa cuenta no existe» de «existe y te equivocaste». El estado HTTP deshacía ese cuidado: bastaba mirar si era 404 o 401 para saberlo, sin leer el cuerpo.Una lista de correos contra
POST /api/v1/auth/loginsepara las cuentas reales de las inventadas a razón de una petición por candidato. No da acceso, pero sí la mitad cara del trabajo: la que no se puede adivinar.No es una idea nueva en esta plataforma. G-053 ya llegó a la misma conclusión para
/api/v1/client/authenticatey colapsó allíAUTH_002yAUTH_003a 401 con mensaje genérico. El endpoint de portal nunca recibió el mismo tratamiento.Por qué AUTH_002 va a 400 y no a 401
Porque aquí el cuerpo ya revela el tenant, y a propósito: el mensaje es «Verifique el código del tenant», distinto del de credenciales y elegido así para quien teclea mal el código de su organización. Colapsar el estado no ocultaría nada que el mensaje siga diciendo. En
/client/authenticate—sin humano al otro lado— sí se colapsan los dos.Esa asimetría es una decisión de producto anterior a este PR y aquí no se cambia. Queda señalada en ADR-0165 §2.3 para que no se lea como olvido: si se decidiera que el portal tampoco debe revelarlo, es un cambio de mensaje y de estado, y merece su propia conversación con producto.
Registrado como ADR-0165 (bilingüe) porque 401 donde el instinto REST pide 404 es exactamente lo que alguien «arregla» de vuelta si el motivo no está escrito.
2. El pacto verificaba un cuerpo que la API dejó de enviar
Declaraba
{ status, title }—ProblemDetails, la forma por defecto de ASP.NET— y esta API respondeLoginErrorResponse:{ code, message, supportReferenceId }. ElsupportReferenceIdes deliberado y es lo único que permite cruzar la queja de una persona con la traza del servidor, así que el contrato se corrige hacia la API, no al revés.De paso, un segundo defecto más callado en el mismo fichero: la aserción de
Content-TypeusabaMatch.Type("application/problem+json"). Un matcher de tipo sobre una cadena casa con cualquier cadena, así que esa línea dabaOKcontra un content type que la API ya no envía. La comprobación existía y no comprobaba nada — la misma forma que las claves de GraphQL de TD-004. Ahora se afirma por valor exacto.Alcance
El front no cambia:
auth.service.tselige el mensaje por elcodedel cuerpo, nunca por el estado. La única rama que mira el estado es el respaldo para cuando no hay cuerpo, y sigue tratando 401 como «verifique sus credenciales».El SDK no se ve afectado: habla con
/api/v1/client/authenticate, otro endpoint, cuyo mapeo no se toca.Registrado como TD-009 (resuelto) en technical-debt.md.
Verificación
Ums.ContractTest— 38/38 (37/38 antes)Ums.Domain.Test— 930/930Ums.Application.Test— 934/934Ums.Presentation.IntegrationTest— 314/316 (2 saltados)dotnet build Ums.sln— 0 erroresQueda pendiente
Reportar aguas arriba:
unimar-umstiene el mismoMapAuthError.🤖 Generated with Claude Code