| """Contract tests — verify API responses match OpenAPI schema. |
| |
| Uses schemathesis to fuzz test that every endpoint returns responses |
| that conform to the declared schema. Catches type mismatches, missing |
| required fields, and incorrect status codes. |
| |
| T21: Run with `pytest tests/contract/ -x` |
| """ |
|
|
| import pytest |
|
|
| |
| schemathesis = pytest.importorskip("schemathesis") |
|
|
| SCHEMA_URL = "http://localhost:8000/openapi.json" |
|
|
|
|
| @pytest.fixture(scope="module") |
| def schema(): |
| """Load the OpenAPI schema from the running server.""" |
| try: |
| return schemathesis.from_uri(SCHEMA_URL) |
| except Exception: |
| pytest.skip("Backend not running at localhost:8000") |
|
|
|
|
| @pytest.fixture(scope="module") |
| def api_state(schema): |
| """Set up Hypothesis strategies for property-based testing.""" |
| return schema.as_state_machine() |
|
|
|
|
| def test_openapi_schema_loads(): |
| """Verify the OpenAPI schema is valid and loadable.""" |
| import httpx |
| resp = httpx.get(SCHEMA_URL) |
| assert resp.status_code == 200, f"OpenAPI schema not available: {resp.status_code}" |
| data = resp.json() |
| assert "paths" in data |
| assert "components" in data |
| assert len(data["paths"]) > 10, "Expected at least 10 endpoints" |
|
|
|
|
| def test_health_endpoint_contract(): |
| """Health endpoint must return 200 with correct schema.""" |
| import httpx |
| resp = httpx.get("http://localhost:8000/health") |
| assert resp.status_code == 200 |
| data = resp.json() |
| assert "status" in data |
| assert "stores" in data |
| assert data["status"] in ("healthy", "degraded", "unhealthy") |
|
|
|
|
| def test_liveness_contract(): |
| """Liveness endpoint must return {status: 'alive'}.""" |
| import httpx |
| resp = httpx.get("http://localhost:8000/live") |
| assert resp.status_code == 200 |
| data = resp.json() |
| assert data["status"] == "alive" |
|
|
|
|
| def test_readiness_contract(): |
| """Readiness endpoint must return {status: 'ready'|'not_ready', checks: {...}}.""" |
| import httpx |
| resp = httpx.get("http://localhost:8000/ready") |
| assert resp.status_code == 200 |
| data = resp.json() |
| assert data["status"] in ("ready", "not_ready") |
| assert "checks" in data |
| assert isinstance(data["checks"], dict) |
|
|
|
|
| def test_metrics_endpoint_serves_prometheus(): |
| """/metrics must return Prometheus exposition format.""" |
| import httpx |
| resp = httpx.get("http://localhost:8000/metrics") |
| assert resp.status_code == 200 |
| assert "HELP" in resp.text or "#" in resp.text, "Not Prometheus format" |
|
|
|
|
| def test_no_endpoint_returns_500_on_valid_input(): |
| """Valid requests to known endpoints should not return 500.""" |
| import httpx |
| endpoints = [ |
| ("GET", "/health"), |
| ("GET", "/live"), |
| ("GET", "/ready"), |
| ("GET", "/metrics"), |
| ] |
| for method, path in endpoints: |
| resp = httpx.request(method, f"http://localhost:8000{path}") |
| assert resp.status_code != 500, f"{method} {path} returned 500" |
|
|