| """ |
| Tests for Stage 86 β OpenAPI examples + SDK README. |
| |
| Coverage: |
| 1. SDK README exists at the right path |
| 2. README mentions the 3 public classes + auth + transport |
| injection + Hebrew quickstart reference |
| 3. OpenAPI schema includes the json_schema_extra examples |
| for each tightened body (7 schemas total) |
| 4. Examples in the schema are well-formed (parse as JSON) |
| 5. /docs and /openapi.json endpoints work end-to-end |
| """ |
| from pathlib import Path |
|
|
| import pytest |
|
|
| pytest.importorskip("fastapi") |
| pytest.importorskip("httpx") |
| from fastapi.testclient import TestClient |
|
|
| from infra import OrgStateService |
| from infra.api import create_app |
| from infra.api.schemas import ( |
| ApiKeyMintBody, |
| CalibrateBody, |
| ObservationsIngestBody, |
| RunAnalysisBody, |
| ScheduleCreateBody, |
| TenantRegisterBody, |
| WebhookCreateBody, |
| ) |
|
|
| SDK_README = Path(__file__).parent.parent / "orgstate_client" / "README.md" |
|
|
|
|
| |
| |
| |
|
|
| def test_sdk_readme_exists(): |
| assert SDK_README.exists(), \ |
| f"SDK README missing at {SDK_README}" |
|
|
|
|
| def test_sdk_readme_mentions_public_classes(): |
| text = SDK_README.read_text(encoding="utf-8") |
| for cls in ("Client", "AdminClient", "ClientError"): |
| assert cls in text, f"SDK README missing reference to {cls}" |
|
|
|
|
| def test_sdk_readme_documents_transport_injection(): |
| """transport injection is the key testability story β |
| the README must show how to plug FastAPI's TestClient in.""" |
| text = SDK_README.read_text(encoding="utf-8") |
| assert "TestClient" in text |
| assert "transport=" in text |
|
|
|
|
| def test_sdk_readme_links_to_hebrew_quickstart_and_runbook(): |
| """The README is part of the customer-facing doc bundle. |
| Cross-link to the rest so navigation works in any tool.""" |
| text = SDK_README.read_text(encoding="utf-8") |
| assert "CUSTOMER_QUICKSTART_HE.md" in text |
| assert "RUNBOOK.md" in text |
|
|
|
|
| def test_sdk_readme_includes_error_handling_example(): |
| text = SDK_README.read_text(encoding="utf-8") |
| assert "ClientError" in text |
| assert "status_code" in text |
| assert "422" in text |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.parametrize("model,expected_field", [ |
| (TenantRegisterBody, "tenant_id"), |
| (CalibrateBody, "observations"), |
| (RunAnalysisBody, "observations"), |
| (ScheduleCreateBody, "connector_type"), |
| (WebhookCreateBody, "url"), |
| (ObservationsIngestBody, "entity_type"), |
| (ApiKeyMintBody, "role"), |
| ]) |
| def test_model_has_example_in_schema(model, expected_field): |
| """Each tightened body should expose an `examples` array in |
| its JSON schema, and the example should mention the field |
| that defines its purpose.""" |
| schema = model.model_json_schema() |
| examples = schema.get("examples", []) |
| assert len(examples) >= 1, \ |
| f"{model.__name__} missing examples in schema" |
| example = examples[0] |
| assert expected_field in example, \ |
| f"{model.__name__} example missing field {expected_field!r}" |
|
|
|
|
| def test_calibrate_example_observations_well_formed(): |
| """The observations example must have the structure that |
| ObservationModel requires (entity_id, day, values) β |
| otherwise a customer copy/pasting from /docs would get a |
| 422 back.""" |
| schema = CalibrateBody.model_json_schema() |
| example = schema["examples"][0] |
| obs = example["observations"] |
| assert isinstance(obs, list) and len(obs) >= 1 |
| for o in obs: |
| assert "entity_id" in o |
| assert "day" in o |
| assert "values" in o and isinstance(o["values"], dict) |
|
|
|
|
| def test_example_actually_validates_against_the_model(): |
| """The strongest contract β the example schema MUST parse |
| against the model itself. Otherwise the /docs example is a |
| lie that breaks the first customer who copies it.""" |
| schema = CalibrateBody.model_json_schema() |
| example = schema["examples"][0] |
| |
| instance = CalibrateBody(**example) |
| assert instance.vertical == "logistics" |
|
|
|
|
| def test_webhook_example_validates(): |
| example = WebhookCreateBody.model_json_schema()["examples"][0] |
| WebhookCreateBody(**example) |
|
|
|
|
| def test_api_key_example_validates(): |
| example = ApiKeyMintBody.model_json_schema()["examples"][0] |
| inst = ApiKeyMintBody(**example) |
| assert inst.role == "operator" |
|
|
|
|
| def test_schedule_example_validates(): |
| example = ScheduleCreateBody.model_json_schema()["examples"][0] |
| ScheduleCreateBody(**example) |
|
|
|
|
| |
| |
| |
|
|
| def _bootstrap(tmp_path): |
| dbfile = str(tmp_path / "openapi.sqlite3") |
| svc = OrgStateService(dbfile) |
| try: |
| svc.register_tenant("acme", "ACME") |
| finally: |
| svc.close() |
| return dbfile |
|
|
|
|
| def test_openapi_json_endpoint_serves_spec(tmp_path): |
| dbfile = _bootstrap(tmp_path) |
| client = TestClient(create_app(dbfile)) |
| r = client.get("/openapi.json") |
| assert r.status_code == 200 |
| spec = r.json() |
| assert spec["info"]["title"] == "OrgState Engine API" |
| assert "paths" in spec |
| assert "components" in spec |
|
|
|
|
| def test_openapi_includes_pydantic_schemas(tmp_path): |
| """The schemas section must reference the body models we |
| wired up in Stage 78 β verifies the schemas are in the |
| spec, not just in the route handlers.""" |
| dbfile = _bootstrap(tmp_path) |
| client = TestClient(create_app(dbfile)) |
| r = client.get("/openapi.json") |
| schemas = r.json()["components"]["schemas"] |
| for name in ("TenantRegisterBody", "WebhookCreateBody", |
| "CalibrateBody", "ScheduleCreateBody"): |
| assert name in schemas, f"OpenAPI missing schema {name}" |
|
|
|
|
| def test_openapi_schemas_carry_examples(tmp_path): |
| """Stage 86 added json_schema_extra examples to the |
| 7 main body schemas. End-to-end verification that the |
| examples make it into the served OpenAPI spec.""" |
| dbfile = _bootstrap(tmp_path) |
| client = TestClient(create_app(dbfile)) |
| schemas = client.get("/openapi.json").json()["components"]["schemas"] |
| for name in ("TenantRegisterBody", "WebhookCreateBody"): |
| assert "examples" in schemas[name] or \ |
| "example" in schemas[name], \ |
| f"{name} has no example in served OpenAPI spec" |
|
|
|
|
| def test_docs_html_endpoint_renders(tmp_path): |
| """Swagger UI is auto-served by FastAPI at /docs. We don't |
| parse the HTML β just verify it 200s and isn't blocked by |
| our security_headers or rate_limit middlewares.""" |
| dbfile = _bootstrap(tmp_path) |
| client = TestClient(create_app(dbfile)) |
| r = client.get("/docs") |
| assert r.status_code == 200 |
| assert "swagger" in r.text.lower() or "redoc" in r.text.lower() \ |
| or "openapi" in r.text.lower() |
|
|