0017. Contract tests y validación OpenAPI en CI¶
Fecha: 2026-09-08 Estado: Propuesto Módulo(s) afectado(s): todos los microservicios (backend y frontend) — transversal
Contexto¶
La regla 5 del ADR 0003 ("contrato antes que código: OpenAPI versionado +
contract tests en CI") y el ADR 0016 (versionado /api/v1/v2/…) definen el
esquema, pero no el mecanismo que lo garantiza. Sin una verificación
automática, una "v1" se rompe en silencio y los consumidores (el frontend que
genera sus tipos desde OpenAPI — ADR 0007/0010 —, o los servicios que llaman
/api/v1/usuarios/{id}) se enteran en producción.
Este ADR define cómo cada servicio publica su OpenAPI, cómo se escriben contract tests y qué se valida en CI para que un cambio rompedor no pase sin subir a la versión siguiente (ADR 0016).
Decisión¶
1. OpenAPI es la única fuente del contrato¶
- Todo servicio FastAPI publica
GET /api/v<major>/openapi.json(generado por el framework). No se versiona a mano un segundo spec. - El contrato "firmado" de cada release es ese JSON, pegado como artefacto en el CI (no como copia editada a mano).
- El frontend genera sus tipos desde ese artifact (ADR 0007/0010): si el contrato cambia, el tipo deja de compilar = signal inmediato.
2. Estructura de los contract tests (por servicio)¶
Cada servicio escribe contract tests sobre su OpenAPI (producer) con dos focos:
a) Estabilidad de v1 (no romper lo publicado):
# tests/test_contract_openapi.py
import json
from openapi_spec_validator import validate_spec
from app.main import app # genera el openapi.json real
# 1) el spec es válido
def test_openapi_valido():
validate_spec(app.openapi())
# 2) los paths críticos están versionados y presentes
def test_paths_versionados():
spec = app.openapi()
for ruta in ["/api/v1/usuarios", "/api/v1/auth/login", "/api/v1/navegacion/panel"]:
assert ruta in spec["paths"], f"falta {ruta}"
b) Diff rompedor contra el release anterior (git):
# compara spec de HEAD vs spec etiquetado del release previo; rompe CI si
# hay cambio rompedor y el major no subió (regla del ADR 0016)
def test_sin_cambios_rompedores_en_v1(previo, actual):
dif = diff_openapi(previo, actual)
assert dif.solo_no_rompedor(), f"rompe contrato: {dif.rompedores}"
Regla automática en CI: cambio rompedor en v1 ⇒ el pipeline falla, no
alcanza con el humano "acordarse"; el ADR 0016 exige publicar v2.
3. Contract tests con consumidores (consumer-driven)¶
Para los contratos entre servicios (p. ej. svc-identidad expone
GET /api/v1/usuarios/{id} que consume el resto), se adoptan contract tests
tipo Pact (pragmático, sin broker todavía):
- El consumidor genera un pact con el request/response esperado y lo versiona en su repo.
- En CI del producer se replayed esos pacts contra su OpenAPI (
pact-verifier) y se valida que lo que los consumidores esperan sigue existiendo. - Los campos que un consumidor no usa no se congelan: el producer puede añadir campos opcionales sin romper (ADR 0016, non-breaking).
4. Qué se valida antes del merge (pipeline estándar)¶
| Chequeo | Herramienta | Fallo ⇒ |
|---|---|---|
| Spec válido + paths versionados | openapi-spec-validator, pytest |
CI rojo |
| Dif rompedor contra release previo | script de diff (semver del ADR 0016) | CI rojo |
| Tipos frontend compilan contra el artifact | openapi-typescript + build |
CI rojo |
| Pacts de consumidores | pact-verifier |
CI rojo |
Rutas con /api/v1/ (sin rutas sueltas) |
grep en el spec (ADR 0016) | CI rojo |
aud/permisos documentados en los seguridad |
revisión de esquema | CI rojo (cuando aplique) |
- La documentación generada (mkdocs) se nutre del OpenAPI publicado por servicio (ADR 0003): un spec rompido se refleja en la doc.
5. Artefactos y etiquetado¶
- Cada release etiqueta el
openapi.jsoncon la versión del path (v1.<commit>), no con la versión del paquete; el diff rompedor se hace contra el últimov1etiquetado. - Después de publicar
v2, el diff dev1se congela (solo mantenimiento) y el chequeo rompedor aplica av2.
Alternativas descartadas¶
- Solo revisión humana del OpenAPI — los cambios se cuelan; el diff rompedor debe ser automático.
- Spec duplicado a mano (yaml propio) — se desincroniza del código; FastAPI ya lo genera.
- Broker de contratos completo (Pactflow) en el día 1 — infraestructura de más para el volumen actual; se replayed pacts desde el repo hasta que justifique un broker.
- Test E2E de cada integración en CI — lento y frágil; los pacts validan el contrato sin levantar toda la plataforma.
Consecuencias¶
- Un contrato público no se rompe en silencio: o es non-breaking, o sube
a
v2(ADR 0016). - La doc generada y los tipos del frontend quedan sincronizados con el OpenAPI real desde el primer merge.
- Costo inicial pequeño por servicio: un módulo de tests + 3 chequeos en el
pipeline; alineado al stack del ADR 0002 de
svc-identidad(pytest, CI). - Los servicios consumidores duermen tranquilos: su pact verifica lo que esperan sin levantar la plataforma.
Complementa al ADR 0016 (esquema /api/v1) y a la regla 5 del ADR 0003.
La generación de tipos en el frontend está en los ADRs 0007 y 0010
transversales; la parte de testing/CI del stack por servicio en el ADR 0002 de
svc-identidad.