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):