diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..89085e7 Binary files /dev/null and b/.coverage differ diff --git a/README.md b/README.md index 4f09fc5..f8323a9 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ All responses are JSON unless otherwise noted. The POST endpoint requires a vali | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | GET | `/api` | none | Returns OpenAPI 3 definition (raw YAML) | +| GET | `/docs` | none | Renders interactive Swagger UI for the OpenAPI spec | | GET | `/token` | none | 303 redirect to external token provider | | GET | `/health` | none | Returns service health and dependency status | | GET | `/topics` | none | Lists available topic names | diff --git a/api.yaml b/api.yaml index f41fc08..2bcb1b3 100644 --- a/api.yaml +++ b/api.yaml @@ -1,22 +1,9 @@ openapi: 3.0.0 info: - title: Event Gate - version: 0.0.0 + title: EventGate API + version: 1.0.0 description: This API provides topic management for an event bus. -servers: - - url: https://{id}-vpce-{vpce}.execute-api.{region}.amazonaws.com/DEV - variables: - id: - default: 01234567ab - description: API Gateway ID - vpce: - default: '01234567abcdef012' - description: VPC endpoint - region: - default: 'af-south-1' - description: AWS Region - paths: /api: get: @@ -30,6 +17,19 @@ paths: schema: type: string + /docs: + get: + summary: Swagger UI documentation page + description: Renders interactive Swagger UI for the OpenAPI spec. + security: [] + responses: + '200': + description: Swagger UI HTML page + content: + text/html: + schema: + type: string + /token: get: summary: Login to the service diff --git a/src/event_gate_lambda.py b/src/event_gate_lambda.py index 7fbae8e..3988717 100644 --- a/src/event_gate_lambda.py +++ b/src/event_gate_lambda.py @@ -76,6 +76,7 @@ # Route to handler function mapping ROUTE_MAP: dict[str, Any] = { "/api": lambda _: handler_api.get_api(), + "/docs": lambda _: handler_api.get_docs(), "/token": lambda _: handler_token.get_token_provider_info(), "/health": lambda _: handler_health.get_health(), "/topics": lambda _: handler_topic.get_topics_list(), diff --git a/src/handlers/handler_api.py b/src/handlers/handler_api.py index 804e43c..beda89a 100644 --- a/src/handlers/handler_api.py +++ b/src/handlers/handler_api.py @@ -14,7 +14,7 @@ # limitations under the License. # -"""OpenAPI specification endpoint handler.""" +"""OpenAPI specification and Swagger UI documentation endpoint handler.""" import logging import os @@ -63,3 +63,35 @@ def get_api(self) -> dict[str, Any]: "headers": {"Content-Type": "application/yaml"}, "body": self.api_spec, } + + def get_docs(self) -> dict[str, Any]: + """Return a Swagger UI HTML page for browsing the API spec. + Returns: + API Gateway response with Swagger UI HTML page. + """ + logger.debug("Handling GET docs.") + html = ( + "\n" + '\n' + "\n" + ' \n' + " EventGate API Docs\n" + ' \n' + "\n" + "\n" + '
\n' + ' \n' + " \n" + "\n" + "" + ) + return { + "statusCode": 200, + "headers": {"Content-Type": "text/html"}, + "body": html, + } diff --git a/src/writers/writer_postgres.py b/src/writers/writer_postgres.py index 06df7c1..5eae61d 100644 --- a/src/writers/writer_postgres.py +++ b/src/writers/writer_postgres.py @@ -192,7 +192,7 @@ def write(self, topic_name: str, message: dict[str, Any], message_key: str = "") if topic_name not in POSTGRES_WRITE_TOPICS: msg = f"Unknown topic for Postgres/{topic_name}" logger.debug(msg) - raise WriteError(msg) + return # no need to pollute the logs and no write should happen for these try: self._execute_with_retry(lambda conn: self._write_topic(conn, topic_name, message), retry=False) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index debd6dc..28bbb35 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -415,6 +415,10 @@ def get_api(self) -> Dict[str, Any]: """Get OpenAPI specification.""" return self.invoke("/api", "GET") + def get_docs(self) -> Dict[str, Any]: + """Get Swagger UI documentation page.""" + return self.invoke("/docs", "GET") + def get_token(self) -> Dict[str, Any]: """Get token provider info.""" return self.invoke("/token", "GET") diff --git a/tests/integration/test_api_endpoint.py b/tests/integration/test_api_endpoint.py index 8e8c259..4be9353 100644 --- a/tests/integration/test_api_endpoint.py +++ b/tests/integration/test_api_endpoint.py @@ -42,3 +42,29 @@ def test_get_api_body_contains_openapi_spec(self, eventgate_client: EventGateTes assert "openapi:" in body assert "paths:" in body assert "/topics" in body + + +class TestDocsEndpoint: + """Tests for the /docs endpoint.""" + + def test_get_docs_returns_200(self, eventgate_client: EventGateTestClient) -> None: + """Test GET /docs returns successful response.""" + response = eventgate_client.get_docs() + + assert 200 == response["statusCode"] + + def test_get_docs_returns_html_content_type(self, eventgate_client: EventGateTestClient) -> None: + """Test GET /docs returns text/html content type.""" + response = eventgate_client.get_docs() + + assert "Content-Type" in response["headers"] + assert "text/html" in response["headers"]["Content-Type"] + + def test_get_docs_body_contains_swagger_ui(self, eventgate_client: EventGateTestClient) -> None: + """Test GET /docs body contains Swagger UI markers and references ./api.""" + response = eventgate_client.get_docs() + + body = response["body"] + assert "swagger" in body.lower() + assert "SwaggerUIBundle" in body + assert "./api" in body diff --git a/tests/unit/handlers/test_handler_api.py b/tests/unit/handlers/test_handler_api.py index 0139ba6..8827eb3 100644 --- a/tests/unit/handlers/test_handler_api.py +++ b/tests/unit/handlers/test_handler_api.py @@ -54,3 +54,33 @@ def test_get_api_returns_correct_response(mocker): assert 200 == response["statusCode"] assert "application/yaml" == response["headers"]["Content-Type"] assert mock_content == response["body"] + + +def test_get_docs_returns_200(mocker): + """Test get_docs returns status code 200.""" + mocker.patch("builtins.open", mock_open(read_data="openapi: 3.0.0")) + handler = HandlerApi().with_api_definition_loaded() + response = handler.get_docs() + + assert 200 == response["statusCode"] + + +def test_get_docs_returns_html_content_type(mocker): + """Test get_docs returns text/html content type.""" + mocker.patch("builtins.open", mock_open(read_data="openapi: 3.0.0")) + handler = HandlerApi().with_api_definition_loaded() + response = handler.get_docs() + + assert "text/html" == response["headers"]["Content-Type"] + + +def test_get_docs_body_contains_swagger_ui_markers(mocker): + """Test get_docs body contains Swagger UI markers and references ./api.""" + mocker.patch("builtins.open", mock_open(read_data="openapi: 3.0.0")) + handler = HandlerApi().with_api_definition_loaded() + response = handler.get_docs() + + body = response["body"] + assert "swagger-ui" in body + assert "SwaggerUIBundle" in body + assert "./api" in body diff --git a/tests/unit/test_event_gate_lambda.py b/tests/unit/test_event_gate_lambda.py index ab62b1e..142f18f 100644 --- a/tests/unit/test_event_gate_lambda.py +++ b/tests/unit/test_event_gate_lambda.py @@ -33,6 +33,17 @@ def test_get_api_endpoint(event_gate_module, make_event): assert "openapi" in resp["body"].lower() +def test_get_docs_endpoint(event_gate_module, make_event): + event = make_event("/docs") + resp = event_gate_module.lambda_handler(event) + assert 200 == resp["statusCode"] + assert "swagger" in resp["body"].lower() + + +def test_docs_route_in_route_map(event_gate_module): + assert "/docs" in event_gate_module.ROUTE_MAP + + def test_internal_error_path(event_gate_module, make_event): with patch.object(event_gate_module.handler_topic, "get_topics_list", side_effect=RuntimeError("boom")): event = make_event("/topics") diff --git a/tests/unit/writers/test_writer_postgres.py b/tests/unit/writers/test_writer_postgres.py index 0777499..c7ae19d 100644 --- a/tests/unit/writers/test_writer_postgres.py +++ b/tests/unit/writers/test_writer_postgres.py @@ -215,7 +215,7 @@ def test_write_skips_when_psycopg2_missing(reset_env, monkeypatch): writer.write("public.cps.za.test", {}) -def test_write_unknown_topic_raises(reset_env, monkeypatch): +def test_write_unknown_topic_skips_silently(reset_env, monkeypatch): store = [] monkeypatch.setattr(pb, "psycopg2", DummyPsycopg(store)) writer = WriterPostgres({}) @@ -224,8 +224,8 @@ def test_write_unknown_topic_raises(reset_env, monkeypatch): "_pg_config", property(lambda self: {"database": "db", "host": "h", "user": "u", "password": "p", "port": 5432}), ) - with pytest.raises(WriteError, match="Unknown topic"): - writer.write("public.cps.za.unknown", {}) + writer.write("public.cps.za.unknown", {}) + assert 0 == len(store) def test_write_success_known_topic(reset_env, monkeypatch):