Skip to content
Binary file added .coverage
Binary file not shown.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
30 changes: 15 additions & 15 deletions api.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/event_gate_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
34 changes: 33 additions & 1 deletion src/handlers/handler_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# limitations under the License.
#

"""OpenAPI specification endpoint handler."""
"""OpenAPI specification and Swagger UI documentation endpoint handler."""

import logging
import os
Expand Down Expand Up @@ -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 = (
"<!DOCTYPE html>\n"
'<html lang="en">\n'
"<head>\n"
' <meta charset="UTF-8">\n'
" <title>EventGate API Docs</title>\n"
' <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">\n'
"</head>\n"
"<body>\n"
' <div id="swagger-ui"></div>\n'
' <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>\n'
" <script>\n"
" window.ui = SwaggerUIBundle({\n"
' url: "./api",\n'
' dom_id: "#swagger-ui"\n'
" });\n"
" </script>\n"
"</body>\n"
"</html>"
)
return {
"statusCode": 200,
"headers": {"Content-Type": "text/html"},
"body": html,
}
2 changes: 1 addition & 1 deletion src/writers/writer_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
26 changes: 26 additions & 0 deletions tests/integration/test_api_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 30 additions & 0 deletions tests/unit/handlers/test_handler_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions tests/unit/test_event_gate_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/writers/test_writer_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({})
Expand All @@ -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):
Expand Down