Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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