Saltar a contenido

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.json con la versión del path (v1.<commit>), no con la versión del paquete; el diff rompedor se hace contra el último v1 etiquetado.
  • Después de publicar v2, el diff de v1 se congela (solo mantenimiento) y el chequeo rompedor aplica a v2.

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.