Skip to content

Commit 2e6746d

Browse files
committed
test: add integration tests for health check and auto-refresh behavior in GatewayService
Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
1 parent b42d401 commit 2e6746d

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

tests/unit/mcpgateway/services/test_gateway_hot_cold_integration.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# Standard
1212
import time
13+
from datetime import datetime, timedelta
1314
from unittest.mock import AsyncMock, MagicMock, patch
1415

1516
# Third-Party
@@ -591,3 +592,165 @@ async def _mark_side_effect(url, poll_type):
591592

592593
# Exception from mark_poll_completed(tool_discovery) must not propagate
593594
await gateway_service._check_single_gateway_health(mock_gateway, user_email="test@example.com")
595+
596+
597+
def _make_http_client_context_manager():
598+
"""Build the async context manager chain used by tests that go through get_isolated_http_client."""
599+
mock_response = MagicMock()
600+
mock_response.status_code = 200
601+
mock_response.raise_for_status = MagicMock()
602+
mock_stream_cm = MagicMock()
603+
mock_stream_cm.__aenter__ = AsyncMock(return_value=mock_response)
604+
mock_stream_cm.__aexit__ = AsyncMock(return_value=False)
605+
mock_http_client = MagicMock()
606+
mock_http_client.stream = MagicMock(return_value=mock_stream_cm)
607+
mock_client_cm = MagicMock()
608+
mock_client_cm.__aenter__ = AsyncMock(return_value=mock_http_client)
609+
mock_client_cm.__aexit__ = AsyncMock(return_value=False)
610+
return mock_client_cm
611+
612+
613+
class TestBranchSpecificMissingLines:
614+
"""Tests that cover lines identified as missing in this branch's changes."""
615+
616+
# ------------------------------------------------------------------
617+
# Line 3590: last_refresh.replace(tzinfo=timezone.utc) — naive datetime
618+
# inside the new `should_auto_refresh` gate added by this branch.
619+
# ------------------------------------------------------------------
620+
621+
@pytest.mark.asyncio
622+
async def test_auto_refresh_naive_last_refresh_at_triggers_tzinfo_fix(self, gateway_service_with_classification, monkeypatch):
623+
"""Naive datetime in last_refresh_at gets UTC tzinfo applied before comparison (line 3590)."""
624+
monkeypatch.setattr("mcpgateway.services.gateway_service.settings.auto_refresh_servers", True)
625+
monkeypatch.setattr("mcpgateway.services.gateway_service.settings.gateway_auto_refresh_interval", 300)
626+
627+
gateway_service, mock_classification = gateway_service_with_classification
628+
# Disable classification so should_auto_refresh=True via the unconditional else branch
629+
gateway_service._classification_service = None
630+
631+
mock_gateway = _make_mock_gateway(url="http://test-server:8000", name="test-gateway")
632+
# Naive datetime (no tzinfo), old enough that time_since_refresh > 300s
633+
mock_gateway.last_refresh_at = datetime.utcnow() - timedelta(hours=2)
634+
mock_gateway.refresh_interval_seconds = None
635+
636+
mock_refresh = AsyncMock(return_value={"added": 0, "updated": 0, "removed": 0})
637+
638+
with (
639+
patch.object(gateway_service, "_refresh_gateway_tools_resources_prompts", mock_refresh),
640+
patch("mcpgateway.services.gateway_service.fresh_db_session") as mock_fresh_db,
641+
patch("mcpgateway.services.gateway_service.get_isolated_http_client") as mock_get_client,
642+
):
643+
mock_session = MagicMock()
644+
mock_fresh_db.return_value.__enter__.return_value = mock_session
645+
mock_session.execute.return_value.scalar_one_or_none.return_value = mock_gateway
646+
mock_session.commit = MagicMock()
647+
mock_get_client.return_value = _make_http_client_context_manager()
648+
649+
# Should not raise; tzinfo fix is applied and refresh proceeds
650+
await gateway_service._check_single_gateway_health(mock_gateway, user_email="test@example.com")
651+
652+
# Refresh must have been called (naive datetime was handled correctly)
653+
mock_refresh.assert_awaited_once()
654+
655+
@pytest.mark.asyncio
656+
async def test_auto_refresh_naive_last_refresh_at_within_interval_skips_refresh(self, gateway_service_with_classification, monkeypatch):
657+
"""Naive datetime recent enough to skip refresh is still handled without error (line 3590 + throttle)."""
658+
monkeypatch.setattr("mcpgateway.services.gateway_service.settings.auto_refresh_servers", True)
659+
monkeypatch.setattr("mcpgateway.services.gateway_service.settings.gateway_auto_refresh_interval", 3600)
660+
661+
gateway_service, mock_classification = gateway_service_with_classification
662+
gateway_service._classification_service = None
663+
664+
mock_gateway = _make_mock_gateway(url="http://test-server:8000", name="test-gateway")
665+
# Naive datetime, refreshed 30 seconds ago (within 3600s interval)
666+
mock_gateway.last_refresh_at = datetime.utcnow() - timedelta(seconds=30)
667+
mock_gateway.refresh_interval_seconds = None
668+
669+
mock_refresh = AsyncMock(return_value={"added": 0, "updated": 0, "removed": 0})
670+
671+
with (
672+
patch.object(gateway_service, "_refresh_gateway_tools_resources_prompts", mock_refresh),
673+
patch("mcpgateway.services.gateway_service.fresh_db_session") as mock_fresh_db,
674+
patch("mcpgateway.services.gateway_service.get_isolated_http_client") as mock_get_client,
675+
):
676+
mock_session = MagicMock()
677+
mock_fresh_db.return_value.__enter__.return_value = mock_session
678+
mock_session.execute.return_value.scalar_one_or_none.return_value = mock_gateway
679+
mock_session.commit = MagicMock()
680+
mock_get_client.return_value = _make_http_client_context_manager()
681+
682+
await gateway_service._check_single_gateway_health(mock_gateway, user_email="test@example.com")
683+
684+
# Not due for refresh yet
685+
mock_refresh.assert_not_awaited()
686+
687+
# ------------------------------------------------------------------
688+
# Lines 3344-3345: decode_auth exception on query_param auth type
689+
# Inside _check_single_gateway_health, modified by this branch.
690+
# ------------------------------------------------------------------
691+
692+
@pytest.mark.asyncio
693+
async def test_health_check_query_param_decryption_failure_proceeds_fail_open(self, gateway_service_with_classification):
694+
"""Health check proceeds when query-param auth decryption raises an exception (lines 3344-3345)."""
695+
gateway_service, mock_classification = gateway_service_with_classification
696+
mock_classification.should_poll_server = AsyncMock(return_value=True)
697+
mock_classification.mark_poll_completed = AsyncMock()
698+
699+
mock_gateway = _make_mock_gateway(url="http://test-server:8000", name="test-gateway")
700+
mock_gateway.auth_type = "query_param"
701+
mock_gateway.auth_query_params = {"api_key": "badly_encrypted_value"}
702+
703+
with (
704+
patch("mcpgateway.services.gateway_service.decode_auth", side_effect=Exception("Decryption failed")),
705+
patch("mcpgateway.services.gateway_service.fresh_db_session") as mock_fresh_db,
706+
patch("mcpgateway.services.gateway_service.get_isolated_http_client") as mock_get_client,
707+
):
708+
mock_session = MagicMock()
709+
mock_fresh_db.return_value.__enter__.return_value = mock_session
710+
mock_session.execute.return_value.scalar_one_or_none.return_value = mock_gateway
711+
mock_session.commit = MagicMock()
712+
mock_get_client.return_value = _make_http_client_context_manager()
713+
714+
# Must not raise; decryption failure is silently logged (line 3344-3345)
715+
await gateway_service._check_single_gateway_health(mock_gateway, user_email="test@example.com")
716+
717+
# Health check still executed (get_isolated_http_client was called)
718+
mock_get_client.assert_called_once()
719+
720+
# ------------------------------------------------------------------
721+
# Line 3398: ssl_context = None (else branch) for https URL with no CA cert
722+
# Inside _check_single_gateway_health, modified by this branch.
723+
# ------------------------------------------------------------------
724+
725+
@pytest.mark.asyncio
726+
async def test_health_check_https_url_no_ca_cert_ssl_context_none(self, gateway_service_with_classification):
727+
"""ssl_context is None for https URL with no CA certificate (line 3398 else branch)."""
728+
gateway_service, mock_classification = gateway_service_with_classification
729+
mock_classification.should_poll_server = AsyncMock(return_value=True)
730+
mock_classification.mark_poll_completed = AsyncMock()
731+
732+
# https URL + no CA certificate → falls into the else: ssl_context = None branch (line 3398)
733+
mock_gateway = _make_mock_gateway(url="https://secure-server:8443", name="test-gateway")
734+
# ca_certificate is None by default in _make_mock_gateway — no valid cert, no ssl override
735+
736+
ssl_context_captured = {}
737+
738+
original_get_isolated = __import__("mcpgateway.services.gateway_service", fromlist=["get_isolated_http_client"])
739+
740+
def capture_ssl_context(*args, **kwargs):
741+
ssl_context_captured["verify"] = kwargs.get("verify")
742+
return _make_http_client_context_manager()
743+
744+
with (
745+
patch("mcpgateway.services.gateway_service.fresh_db_session") as mock_fresh_db,
746+
patch("mcpgateway.services.gateway_service.get_isolated_http_client", side_effect=capture_ssl_context),
747+
):
748+
mock_session = MagicMock()
749+
mock_fresh_db.return_value.__enter__.return_value = mock_session
750+
mock_session.execute.return_value.scalar_one_or_none.return_value = mock_gateway
751+
mock_session.commit = MagicMock()
752+
753+
await gateway_service._check_single_gateway_health(mock_gateway, user_email="test@example.com")
754+
755+
# ssl_context should be None (the else branch at line 3398)
756+
assert ssl_context_captured.get("verify") is None

0 commit comments

Comments
 (0)