From 61a9ba227303317366f98ed0f36ebe4b74a860c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:58:15 +0000 Subject: [PATCH 1/5] Initial plan From b9849d917fb39f244660ef94bbb48b794e434c93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:07:09 +0000 Subject: [PATCH 2/5] Add /health endpoint for service monitoring - Created HandlerHealth class with dependency checks for Kafka, EventBridge, and PostgreSQL - Added /health route to lambda_handler - Implemented uptime tracking and status reporting - Added comprehensive tests for healthy and degraded states - Updated OpenAPI spec with /health endpoint documentation Co-authored-by: tmikula-dev <72911271+tmikula-dev@users.noreply.github.com> --- conf/api.yaml | 35 ++++++ src/event_gate_lambda.py | 6 + src/handlers/handler_health.py | 106 +++++++++++++++++ tests/handlers/test_handler_health.py | 158 ++++++++++++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 src/handlers/handler_health.py create mode 100644 tests/handlers/test_handler_health.py diff --git a/conf/api.yaml b/conf/api.yaml index 46dc63d..b1f2b1d 100644 --- a/conf/api.yaml +++ b/conf/api.yaml @@ -38,6 +38,41 @@ paths: '303': description: Redirect to actual address of Loing service which performs auth up to its capabilities + /health: + get: + summary: Service health check + description: Service health and dependency status check + responses: + '200': + description: Service is healthy + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ok + uptime_seconds: + type: integer + example: 12345 + '503': + description: Service is degraded + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: degraded + details: + type: object + additionalProperties: + type: string + example: + kafka: not_initialized + /topics: get: summary: Get a list of topics diff --git a/src/event_gate_lambda.py b/src/event_gate_lambda.py index a0fcefa..38f0088 100644 --- a/src/event_gate_lambda.py +++ b/src/event_gate_lambda.py @@ -26,6 +26,7 @@ from src.handlers.handler_token import HandlerToken from src.handlers.handler_topic import HandlerTopic +from src.handlers.handler_health import HandlerHealth from src.utils.constants import SSL_CA_BUNDLE_KEY from src.utils.utils import build_error_response from src.writers import writer_eventbridge, writer_kafka, writer_postgres @@ -85,6 +86,9 @@ # Initialize topic handler and load topic schemas handler_topic = HandlerTopic(CONF_DIR, ACCESS, handler_token).load_topic_schemas() +# Initialize health handler +handler_health = HandlerHealth(logger, config) + def get_api() -> Dict[str, Any]: """Return the OpenAPI specification text.""" @@ -108,6 +112,8 @@ def lambda_handler(event: Dict[str, Any], _context: Any = None) -> Dict[str, Any return get_api() if resource == "/token": return handler_token.get_token_provider_info() + if resource == "/health": + return handler_health.get_health() if resource == "/topics": return handler_topic.get_topics_list() if resource == "/topics/{topic_name}": diff --git a/src/handlers/handler_health.py b/src/handlers/handler_health.py new file mode 100644 index 0000000..a272d49 --- /dev/null +++ b/src/handlers/handler_health.py @@ -0,0 +1,106 @@ +# +# Copyright 2025 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module provides the HandlerHealth class for service health monitoring. +""" +import json +import logging +import os +from datetime import datetime, timezone +from typing import Dict, Any + +from src.writers import writer_eventbridge, writer_kafka, writer_postgres + +logger = logging.getLogger(__name__) +log_level = os.environ.get("LOG_LEVEL", "INFO") +logger.setLevel(log_level) + + +class HandlerHealth: + """ + HandlerHealth manages service health checks and dependency status monitoring. + """ + + def __init__(self, logger_instance: logging.Logger, config: Dict[str, Any]): + """ + Initialize the health handler. + + Args: + logger_instance: Shared application logger. + config: Configuration dictionary. + """ + self.logger = logger_instance + self.config = config + self.start_time = datetime.now(timezone.utc) + + def get_health(self) -> Dict[str, Any]: + """ + Check service health and return status. + + Performs lightweight dependency checks by verifying that writer STATE + dictionaries are properly initialized with required keys. + + Returns: + Dict[str, Any]: API Gateway response with health status. + - 200: All dependencies healthy + - 503: One or more dependencies not initialized + """ + logger.debug("Handling GET Health") + + details: Dict[str, str] = {} + all_healthy = True + + # Check Kafka writer STATE + kafka_state = writer_kafka.STATE + if not all(key in kafka_state for key in ["logger", "producer"]): + details["kafka"] = "not_initialized" + all_healthy = False + logger.debug("Kafka writer not properly initialized") + + # Check EventBridge writer STATE + eventbridge_state = writer_eventbridge.STATE + if not all(key in eventbridge_state for key in ["logger", "client", "event_bus_arn"]): + details["eventbridge"] = "not_initialized" + all_healthy = False + logger.debug("EventBridge writer not properly initialized") + + # Check PostgreSQL writer - it uses global logger variable and POSTGRES dict + # Just verify the module is accessible (init is always called in event_gate_lambda) + try: + _ = writer_postgres.logger + except AttributeError: + details["postgres"] = "not_initialized" + all_healthy = False + logger.debug("PostgreSQL writer not accessible") + + # Calculate uptime + uptime_seconds = int((datetime.now(timezone.utc) - self.start_time).total_seconds()) + + if all_healthy: + logger.debug("Health check passed - all dependencies healthy") + return { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps({"status": "ok", "uptime_seconds": uptime_seconds}), + } + + logger.debug("Health check degraded - some dependencies not initialized: %s", details) + return { + "statusCode": 503, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps({"status": "degraded", "details": details}), + } diff --git a/tests/handlers/test_handler_health.py b/tests/handlers/test_handler_health.py new file mode 100644 index 0000000..d15fc7d --- /dev/null +++ b/tests/handlers/test_handler_health.py @@ -0,0 +1,158 @@ +# +# Copyright 2025 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +from unittest.mock import MagicMock, patch +import logging + +from src.handlers.handler_health import HandlerHealth + + +## get_health() - healthy state +def test_get_health_all_dependencies_healthy(): + """Health check returns 200 when all writer STATEs are properly initialized.""" + logger = logging.getLogger("test") + config = {} + handler = HandlerHealth(logger, config) + + # Mock all writers as healthy + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger, "producer": MagicMock()}), + patch( + "src.handlers.handler_health.writer_eventbridge.STATE", + { + "logger": logger, + "client": MagicMock(), + "event_bus_arn": "arn:aws:events:us-east-1:123456789012:event-bus/my-bus", + }, + ), + patch("src.handlers.handler_health.writer_postgres.logger", logger), + ): + response = handler.get_health() + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["status"] == "ok" + assert "uptime_seconds" in body + assert isinstance(body["uptime_seconds"], int) + assert body["uptime_seconds"] >= 0 + + +## get_health() - degraded state - kafka +def test_get_health_kafka_not_initialized(): + """Health check returns 503 when Kafka writer is not initialized.""" + logger = logging.getLogger("test") + config = {} + handler = HandlerHealth(logger, config) + + # Mock Kafka as not initialized (missing producer key) + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger}), + patch( + "src.handlers.handler_health.writer_eventbridge.STATE", + {"logger": logger, "client": MagicMock(), "event_bus_arn": "arn"}, + ), + patch("src.handlers.handler_health.writer_postgres.logger", logger), + ): + response = handler.get_health() + + assert response["statusCode"] == 503 + body = json.loads(response["body"]) + assert body["status"] == "degraded" + assert "details" in body + assert "kafka" in body["details"] + assert body["details"]["kafka"] == "not_initialized" + + +## get_health() - degraded state - eventbridge +def test_get_health_eventbridge_not_initialized(): + """Health check returns 503 when EventBridge writer is not initialized.""" + logger = logging.getLogger("test") + config = {} + handler = HandlerHealth(logger, config) + + # Mock EventBridge as not initialized (missing client key) + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger, "producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"logger": logger}), + patch("src.handlers.handler_health.writer_postgres.logger", logger), + ): + response = handler.get_health() + + assert response["statusCode"] == 503 + body = json.loads(response["body"]) + assert body["status"] == "degraded" + assert "eventbridge" in body["details"] + assert body["details"]["eventbridge"] == "not_initialized" + + +## get_health() - degraded state - multiple failures +def test_get_health_multiple_dependencies_not_initialized(): + """Health check returns 503 when multiple writers are not initialized.""" + logger = logging.getLogger("test") + config = {} + handler = HandlerHealth(logger, config) + + # Mock multiple writers as not initialized + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {}), + patch("src.handlers.handler_health.writer_postgres", spec=[]), # spec=[] makes logger not exist + ): + response = handler.get_health() + + assert response["statusCode"] == 503 + body = json.loads(response["body"]) + assert body["status"] == "degraded" + assert len(body["details"]) >= 2 # At least kafka and eventbridge + assert "kafka" in body["details"] + assert "eventbridge" in body["details"] + + +## get_health() - uptime calculation +def test_get_health_uptime_is_positive(): + """Verify uptime_seconds is calculated and is a positive integer.""" + logger = logging.getLogger("test") + config = {} + handler = HandlerHealth(logger, config) + + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger, "producer": MagicMock()}), + patch( + "src.handlers.handler_health.writer_eventbridge.STATE", + {"logger": logger, "client": MagicMock(), "event_bus_arn": "arn"}, + ), + patch("src.handlers.handler_health.writer_postgres.logger", logger), + ): + response = handler.get_health() + + body = json.loads(response["body"]) + assert "uptime_seconds" in body + assert isinstance(body["uptime_seconds"], int) + assert body["uptime_seconds"] >= 0 + + +## Integration test with event_gate_module +def test_health_endpoint_integration(event_gate_module, make_event): + """Test /health endpoint through lambda_handler.""" + event = make_event("/health") + resp = event_gate_module.lambda_handler(event) + + # Should return 200 since writers are mocked as initialized in conftest + assert resp["statusCode"] == 200 + body = json.loads(resp["body"]) + assert body["status"] == "ok" + assert "uptime_seconds" in body From ae76d690cb6f0390da2c2582a9198f198e9d3403 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:09:07 +0000 Subject: [PATCH 3/5] Fix code review issues - use instance logger instead of global - Changed all logger.debug() calls to self.logger.debug() - Removed unused global logger initialization - All tests pass, pylint 10.00/10, mypy clean Co-authored-by: tmikula-dev <72911271+tmikula-dev@users.noreply.github.com> --- src/handlers/handler_health.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/handlers/handler_health.py b/src/handlers/handler_health.py index a272d49..4666ce8 100644 --- a/src/handlers/handler_health.py +++ b/src/handlers/handler_health.py @@ -19,16 +19,11 @@ """ import json import logging -import os from datetime import datetime, timezone from typing import Dict, Any from src.writers import writer_eventbridge, writer_kafka, writer_postgres -logger = logging.getLogger(__name__) -log_level = os.environ.get("LOG_LEVEL", "INFO") -logger.setLevel(log_level) - class HandlerHealth: """ @@ -59,7 +54,7 @@ def get_health(self) -> Dict[str, Any]: - 200: All dependencies healthy - 503: One or more dependencies not initialized """ - logger.debug("Handling GET Health") + self.logger.debug("Handling GET Health") details: Dict[str, str] = {} all_healthy = True @@ -69,14 +64,14 @@ def get_health(self) -> Dict[str, Any]: if not all(key in kafka_state for key in ["logger", "producer"]): details["kafka"] = "not_initialized" all_healthy = False - logger.debug("Kafka writer not properly initialized") + self.logger.debug("Kafka writer not properly initialized") # Check EventBridge writer STATE eventbridge_state = writer_eventbridge.STATE if not all(key in eventbridge_state for key in ["logger", "client", "event_bus_arn"]): details["eventbridge"] = "not_initialized" all_healthy = False - logger.debug("EventBridge writer not properly initialized") + self.logger.debug("EventBridge writer not properly initialized") # Check PostgreSQL writer - it uses global logger variable and POSTGRES dict # Just verify the module is accessible (init is always called in event_gate_lambda) @@ -85,20 +80,20 @@ def get_health(self) -> Dict[str, Any]: except AttributeError: details["postgres"] = "not_initialized" all_healthy = False - logger.debug("PostgreSQL writer not accessible") + self.logger.debug("PostgreSQL writer not accessible") # Calculate uptime uptime_seconds = int((datetime.now(timezone.utc) - self.start_time).total_seconds()) if all_healthy: - logger.debug("Health check passed - all dependencies healthy") + self.logger.debug("Health check passed - all dependencies healthy") return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"status": "ok", "uptime_seconds": uptime_seconds}), } - logger.debug("Health check degraded - some dependencies not initialized: %s", details) + self.logger.debug("Health check degraded - some dependencies not initialized: %s", details) return { "statusCode": 503, "headers": {"Content-Type": "application/json"}, From 3bf8765f573c6098621390d9498c729f2da54fa7 Mon Sep 17 00:00:00 2001 From: "Tobias.Mikula" Date: Mon, 5 Jan 2026 15:15:08 +0100 Subject: [PATCH 4/5] Human rework of the PR task --- conf/api.yaml | 6 +- src/event_gate_lambda.py | 2 +- src/handlers/handler_health.py | 83 +++++++-------- tests/handlers/test_handler_health.py | 145 +++++++++++++------------- 4 files changed, 117 insertions(+), 119 deletions(-) diff --git a/conf/api.yaml b/conf/api.yaml index b1f2b1d..199c247 100644 --- a/conf/api.yaml +++ b/conf/api.yaml @@ -66,12 +66,14 @@ paths: status: type: string example: degraded - details: + failures: type: object additionalProperties: type: string example: - kafka: not_initialized + eventbridge: client not initialized + kafka: producer not initialized + postgres: host not configured /topics: get: diff --git a/src/event_gate_lambda.py b/src/event_gate_lambda.py index 38f0088..16c98eb 100644 --- a/src/event_gate_lambda.py +++ b/src/event_gate_lambda.py @@ -87,7 +87,7 @@ handler_topic = HandlerTopic(CONF_DIR, ACCESS, handler_token).load_topic_schemas() # Initialize health handler -handler_health = HandlerHealth(logger, config) +handler_health = HandlerHealth() def get_api() -> Dict[str, Any]: diff --git a/src/handlers/handler_health.py b/src/handlers/handler_health.py index 4666ce8..0db951a 100644 --- a/src/handlers/handler_health.py +++ b/src/handlers/handler_health.py @@ -19,83 +19,74 @@ """ import json import logging +import os from datetime import datetime, timezone from typing import Dict, Any from src.writers import writer_eventbridge, writer_kafka, writer_postgres +logger = logging.getLogger(__name__) +log_level = os.environ.get("LOG_LEVEL", "INFO") +logger.setLevel(log_level) + class HandlerHealth: """ HandlerHealth manages service health checks and dependency status monitoring. """ - def __init__(self, logger_instance: logging.Logger, config: Dict[str, Any]): - """ - Initialize the health handler. - - Args: - logger_instance: Shared application logger. - config: Configuration dictionary. - """ - self.logger = logger_instance - self.config = config - self.start_time = datetime.now(timezone.utc) + def __init__(self): + self.start_time: datetime = datetime.now(timezone.utc) def get_health(self) -> Dict[str, Any]: """ Check service health and return status. - Performs lightweight dependency checks by verifying that writer STATE - dictionaries are properly initialized with required keys. - Returns: Dict[str, Any]: API Gateway response with health status. - 200: All dependencies healthy - 503: One or more dependencies not initialized """ - self.logger.debug("Handling GET Health") - - details: Dict[str, str] = {} - all_healthy = True - - # Check Kafka writer STATE - kafka_state = writer_kafka.STATE - if not all(key in kafka_state for key in ["logger", "producer"]): - details["kafka"] = "not_initialized" - all_healthy = False - self.logger.debug("Kafka writer not properly initialized") - - # Check EventBridge writer STATE - eventbridge_state = writer_eventbridge.STATE - if not all(key in eventbridge_state for key in ["logger", "client", "event_bus_arn"]): - details["eventbridge"] = "not_initialized" - all_healthy = False - self.logger.debug("EventBridge writer not properly initialized") - - # Check PostgreSQL writer - it uses global logger variable and POSTGRES dict - # Just verify the module is accessible (init is always called in event_gate_lambda) - try: - _ = writer_postgres.logger - except AttributeError: - details["postgres"] = "not_initialized" - all_healthy = False - self.logger.debug("PostgreSQL writer not accessible") + logger.debug("Handling GET Health") + + failures: Dict[str, str] = {} + + # Check Kafka writer + if writer_kafka.STATE.get("producer") is None: + failures["kafka"] = "producer not initialized" + + # Check EventBridge writer + eventbus_arn = writer_eventbridge.STATE.get("event_bus_arn") + eventbridge_client = writer_eventbridge.STATE.get("client") + if eventbus_arn: + if eventbridge_client is None: + failures["eventbridge"] = "client not initialized" + + # Check PostgreSQL writer + postgres_config = writer_postgres.POSTGRES + if postgres_config.get("database"): + if not postgres_config.get("host"): + failures["postgres"] = "host not configured" + elif not postgres_config.get("user"): + failures["postgres"] = "user not configured" + elif not postgres_config.get("password"): + failures["postgres"] = "password not configured" + elif not postgres_config.get("port"): + failures["postgres"] = "port not configured" - # Calculate uptime uptime_seconds = int((datetime.now(timezone.utc) - self.start_time).total_seconds()) - if all_healthy: - self.logger.debug("Health check passed - all dependencies healthy") + if not failures: + logger.debug("Health check passed") return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"status": "ok", "uptime_seconds": uptime_seconds}), } - self.logger.debug("Health check degraded - some dependencies not initialized: %s", details) + logger.debug("Health check degraded: %s", failures) return { "statusCode": 503, "headers": {"Content-Type": "application/json"}, - "body": json.dumps({"status": "degraded", "details": details}), + "body": json.dumps({"status": "degraded", "failures": failures}), } diff --git a/tests/handlers/test_handler_health.py b/tests/handlers/test_handler_health.py index d15fc7d..472ee9f 100644 --- a/tests/handlers/test_handler_health.py +++ b/tests/handlers/test_handler_health.py @@ -16,30 +16,39 @@ import json from unittest.mock import MagicMock, patch -import logging from src.handlers.handler_health import HandlerHealth +### get_health() -## get_health() - healthy state -def test_get_health_all_dependencies_healthy(): - """Health check returns 200 when all writer STATEs are properly initialized.""" - logger = logging.getLogger("test") - config = {} - handler = HandlerHealth(logger, config) +## Minimal healthy state (just kafka) +def test_get_health_minimal_kafka_healthy(): + """Health check returns 200 when Kafka is initialized and optional writers are disabled.""" + handler = HandlerHealth() - # Mock all writers as healthy with ( - patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger, "producer": MagicMock()}), - patch( - "src.handlers.handler_health.writer_eventbridge.STATE", - { - "logger": logger, - "client": MagicMock(), - "event_bus_arn": "arn:aws:events:us-east-1:123456789012:event-bus/my-bus", - }, - ), - patch("src.handlers.handler_health.writer_postgres.logger", logger), + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"client": None, "event_bus_arn": ""}), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", {"database": ""}), + ): + response = handler.get_health() + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["status"] == "ok" + assert "uptime_seconds" in body + + +## Healthy state with all writers enabled +def test_get_health_all_writers_enabled_and_healthy(): + """Health check returns 200 when all writers are enabled and properly configured.""" + handler = HandlerHealth() + postgres_config = {"database": "db", "host": "localhost", "user": "user", "password": "pass", "port": "5432"} + + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"client": MagicMock(), "event_bus_arn": "arn"}), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", postgres_config), ): response = handler.get_health() @@ -47,95 +56,91 @@ def test_get_health_all_dependencies_healthy(): body = json.loads(response["body"]) assert body["status"] == "ok" assert "uptime_seconds" in body - assert isinstance(body["uptime_seconds"], int) - assert body["uptime_seconds"] >= 0 -## get_health() - degraded state - kafka +## Degraded state with all writers enabled def test_get_health_kafka_not_initialized(): """Health check returns 503 when Kafka writer is not initialized.""" - logger = logging.getLogger("test") - config = {} - handler = HandlerHealth(logger, config) + handler = HandlerHealth() + postgres_config = {"database": "db", "host": "", "user": "", "password": "", "port": ""} - # Mock Kafka as not initialized (missing producer key) with ( - patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger}), + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": None}), patch( "src.handlers.handler_health.writer_eventbridge.STATE", - {"logger": logger, "client": MagicMock(), "event_bus_arn": "arn"}, + {"client": None, "event_bus_arn": "arn:aws:events:us-east-1:123:event-bus/bus"} ), - patch("src.handlers.handler_health.writer_postgres.logger", logger), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", postgres_config), ): response = handler.get_health() assert response["statusCode"] == 503 body = json.loads(response["body"]) assert body["status"] == "degraded" - assert "details" in body - assert "kafka" in body["details"] - assert body["details"]["kafka"] == "not_initialized" + assert "kafka" in body["failures"] + assert "eventbridge" in body["failures"] + assert "postgres" in body["failures"] -## get_health() - degraded state - eventbridge -def test_get_health_eventbridge_not_initialized(): - """Health check returns 503 when EventBridge writer is not initialized.""" - logger = logging.getLogger("test") - config = {} - handler = HandlerHealth(logger, config) +## Healthy when eventbridge is disabled +def test_get_health_eventbridge_disabled(): + """Health check returns 200 when EventBridge is disabled (empty event_bus_arn).""" + handler = HandlerHealth() + postgres_config = {"database": "db", "host": "localhost", "user": "user", "password": "pass", "port": "5432"} - # Mock EventBridge as not initialized (missing client key) with ( - patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger, "producer": MagicMock()}), - patch("src.handlers.handler_health.writer_eventbridge.STATE", {"logger": logger}), - patch("src.handlers.handler_health.writer_postgres.logger", logger), + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"client": None, "event_bus_arn": ""}), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", postgres_config), ): response = handler.get_health() - assert response["statusCode"] == 503 - body = json.loads(response["body"]) - assert body["status"] == "degraded" - assert "eventbridge" in body["details"] - assert body["details"]["eventbridge"] == "not_initialized" + assert response["statusCode"] == 200 + + +## Healthy when postgres is disabled +def test_get_health_postgres_disabled(): + """Health check returns 200 when PostgreSQL is disabled (empty database).""" + handler = HandlerHealth() + + with ( + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"client": MagicMock(), "event_bus_arn": "arn"}), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", {"database": ""}), + ): + response = handler.get_health() + + assert response["statusCode"] == 200 -## get_health() - degraded state - multiple failures -def test_get_health_multiple_dependencies_not_initialized(): - """Health check returns 503 when multiple writers are not initialized.""" - logger = logging.getLogger("test") - config = {} - handler = HandlerHealth(logger, config) +## Degraded state - postgres host not configured +def test_get_health_postgres_host_not_configured(): + """Health check returns 503 when PostgreSQL host is not configured.""" + handler = HandlerHealth() + postgres_config = {"database": "db", "host": "", "user": "user", "password": "pass", "port": "5432"} - # Mock multiple writers as not initialized with ( - patch("src.handlers.handler_health.writer_kafka.STATE", {}), - patch("src.handlers.handler_health.writer_eventbridge.STATE", {}), - patch("src.handlers.handler_health.writer_postgres", spec=[]), # spec=[] makes logger not exist + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"client": MagicMock(), "event_bus_arn": "arn"}), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", postgres_config), ): response = handler.get_health() assert response["statusCode"] == 503 body = json.loads(response["body"]) - assert body["status"] == "degraded" - assert len(body["details"]) >= 2 # At least kafka and eventbridge - assert "kafka" in body["details"] - assert "eventbridge" in body["details"] + assert body["failures"]["postgres"] == "host not configured" -## get_health() - uptime calculation +## Uptime calculation def test_get_health_uptime_is_positive(): """Verify uptime_seconds is calculated and is a positive integer.""" - logger = logging.getLogger("test") - config = {} - handler = HandlerHealth(logger, config) + handler = HandlerHealth() + postgres_config = {"database": "db", "host": "localhost", "user": "user", "password": "pass", "port": "5432"} with ( - patch("src.handlers.handler_health.writer_kafka.STATE", {"logger": logger, "producer": MagicMock()}), - patch( - "src.handlers.handler_health.writer_eventbridge.STATE", - {"logger": logger, "client": MagicMock(), "event_bus_arn": "arn"}, - ), - patch("src.handlers.handler_health.writer_postgres.logger", logger), + patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": MagicMock()}), + patch("src.handlers.handler_health.writer_eventbridge.STATE", {"client": MagicMock(), "event_bus_arn": "arn"}), + patch("src.handlers.handler_health.writer_postgres.POSTGRES", postgres_config), ): response = handler.get_health() From f1760e3970d290d9cf5c87ec31c7c61005cd8ea8 Mon Sep 17 00:00:00 2001 From: "Tobias.Mikula" Date: Mon, 5 Jan 2026 15:16:36 +0100 Subject: [PATCH 5/5] Black formatting improvement --- tests/handlers/test_handler_health.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/handlers/test_handler_health.py b/tests/handlers/test_handler_health.py index 472ee9f..ca3bfbb 100644 --- a/tests/handlers/test_handler_health.py +++ b/tests/handlers/test_handler_health.py @@ -21,6 +21,7 @@ ### get_health() + ## Minimal healthy state (just kafka) def test_get_health_minimal_kafka_healthy(): """Health check returns 200 when Kafka is initialized and optional writers are disabled.""" @@ -68,7 +69,7 @@ def test_get_health_kafka_not_initialized(): patch("src.handlers.handler_health.writer_kafka.STATE", {"producer": None}), patch( "src.handlers.handler_health.writer_eventbridge.STATE", - {"client": None, "event_bus_arn": "arn:aws:events:us-east-1:123:event-bus/bus"} + {"client": None, "event_bus_arn": "arn:aws:events:us-east-1:123:event-bus/bus"}, ), patch("src.handlers.handler_health.writer_postgres.POSTGRES", postgres_config), ):