"""Unit tests for Loki URL resolution and caching service.""" from unittest.mock import AsyncMock, patch import pytest from src.models.cloud_access import CloudAccessEntry from src.services.loki import LokiClient from src.services.loki_resolve import ( add_env_to_query, get_loki_client_for_cluster, ) from src.utils import cloud_access_store @pytest.fixture(autouse=True) def reset_cloud_access_store(monkeypatch): """Isolate tests by replacing the module-level store with a fresh dict.""" store = {} monkeypatch.setattr(cloud_access_store, "_store", store) yield store @pytest.fixture def mock_get_or_resolve_metrics_url(): """Mock the get_or_resolve_metrics_url function.""" with patch( "src.services.loki_resolve.get_or_resolve_metrics_url", new_callable=AsyncMock, ) as mock: yield mock @pytest.fixture def mock_get_server_auth0_token(): """Mock the get_server_auth0_token function.""" with patch( "src.services.loki_resolve.get_server_auth0_token", return_value="test-auth0-token", ): yield class TestGetLokiClientForCluster: """get_loki_client_for_cluster returns LokiClient with correct URL and caching.""" @pytest.mark.asyncio async def test_returns_client_from_cache_when_logs_url_exists( self, reset_cloud_access_store, mock_get_server_auth0_token ): """Test that cached logs_url is used when available.""" cached_entry = CloudAccessEntry( logs_url="https://www.ds.us-east-1.aws.observability.tidbcloud.com/loki/self-monitoring" ) cloud_access_store.set_entry("cluster-123", "prod", "dedicated", cached_entry) client, region = await get_loki_client_for_cluster( cluster_id="cluster-123", env="prod", deploy_type="dedicated", ) assert isinstance(client, LokiClient) assert ( client._base_url == "https://www.ds.us-east-1.aws.observability.tidbcloud.com/loki/self-monitoring" ) assert client.timeout == 30.0 assert region == "us-east-1" @pytest.mark.asyncio async def test_builds_url_from_internal_read_uri( self, reset_cloud_access_store, mock_get_or_resolve_metrics_url, mock_get_server_auth0_token, ): """Test that Loki URL is built from internalReadUri.""" mock_get_or_resolve_metrics_url.return_value = AsyncMock( url="https://www.ds.ap-southeast-2-ng.aws.observability.tidbcloud.com/internal/v2/metrics/019c2af7-1429-7487-a27d-3a0cde78dbd3/api/v1/" ) client, region = await get_loki_client_for_cluster( cluster_id="cluster-456", env="prod", deploy_type="dedicated", ) assert isinstance(client, LokiClient) expected_url = ( "https://www.ds.ap-southeast-2-ng.aws.observability.tidbcloud.com/loki/self-monitoring" ) assert client._base_url == expected_url assert region == "ap-southeast-2-ng" cached = cloud_access_store.get_entry("cluster-456", "prod", "dedicated") assert cached is not None assert cached.logs_url == expected_url @pytest.mark.asyncio async def test_uses_auth0_token( self, reset_cloud_access_store, mock_get_or_resolve_metrics_url, mock_get_server_auth0_token, ): """Test that auth0 token is used for Loki client.""" mock_get_or_resolve_metrics_url.return_value = AsyncMock( url="https://www.ds.us-east-1.aws.observability.tidbcloud.com/internal/v2/metrics/cluster/api/v1/" ) client, region = await get_loki_client_for_cluster( cluster_id="cluster-123", env="prod", deploy_type="dedicated", ) assert client._token == "test-auth0-token" @pytest.mark.asyncio async def test_uses_custom_timeout( self, reset_cloud_access_store, mock_get_or_resolve_metrics_url, mock_get_server_auth0_token, ): """Test that custom timeout is passed to Loki client.""" mock_get_or_resolve_metrics_url.return_value = AsyncMock( url="https://www.ds.us-east-1.aws.observability.tidbcloud.com/internal/v2/metrics/cluster/api/v1/" ) client, region = await get_loki_client_for_cluster( cluster_id="cluster-123", env="prod", deploy_type="dedicated", timeout=60.0, ) assert client.timeout == 60.0 @pytest.mark.asyncio async def test_preserves_existing_cache_entry_fields( self, reset_cloud_access_store, mock_get_or_resolve_metrics_url, mock_get_server_auth0_token, ): """Test that existing cache entry fields are preserved.""" from src.models.cloud_access import S3Access s3_access = S3Access(provider="aws", secret_access_key="secret123", region="us-east-1") existing_entry = CloudAccessEntry( s3_access=s3_access, metrics_url="https://metrics.example.com", metrics_token="metrics-token", s3_url="https://s3.example.com", ) cloud_access_store.set_entry("cluster-123", "prod", "dedicated", existing_entry) mock_get_or_resolve_metrics_url.return_value = AsyncMock( url="https://www.ds.us-east-1.aws.observability.tidbcloud.com/internal/v2/metrics/cluster/api/v1/" ) client, region = await get_loki_client_for_cluster( cluster_id="cluster-123", env="prod", deploy_type="dedicated", ) assert isinstance(client, LokiClient) cached = cloud_access_store.get_entry("cluster-123", "prod", "dedicated") assert cached is not None assert cached.logs_url is not None assert cached.s3_access == s3_access assert cached.metrics_url == "https://metrics.example.com" assert cached.metrics_token == "metrics-token" assert cached.s3_url == "https://s3.example.com" @pytest.mark.asyncio async def test_strips_whitespace_from_parameters( self, reset_cloud_access_store, mock_get_or_resolve_metrics_url, mock_get_server_auth0_token, ): """Test that parameters are stripped of whitespace.""" mock_get_or_resolve_metrics_url.return_value = AsyncMock( url="https://www.ds.us-east-1.aws.observability.tidbcloud.com/internal/v2/metrics/cluster/api/v1/" ) client, region = await get_loki_client_for_cluster( cluster_id=" cluster-123 ", env=" prod ", deploy_type=" dedicated ", ) assert isinstance(client, LokiClient) cached = cloud_access_store.get_entry("cluster-123", "prod", "dedicated") assert cached is not None class TestAddEnvToQuery: """Tests for add_env_to_query function.""" def test_adds_env_to_query_without_existing_labels(self): """Test adding env to query with no existing labels.""" result = add_env_to_query('{app="mysql"}', "prod") assert result == '{app="mysql", cluster_env="prod"}' def test_adds_env_to_query_with_existing_labels(self): """Test adding env to query with existing labels.""" result = add_env_to_query('{app="mysql", level="error"}', "staging") assert result == '{app="mysql", level="error", cluster_env="staging"}' def test_does_not_duplicate_env_if_already_present(self): """Test that env is not added if already present.""" result = add_env_to_query('{app="mysql", env="prod"}', "staging") assert result == '{app="mysql", env="prod", cluster_env="staging"}' def test_handles_query_without_braces(self): """Test handling query without braces.""" result = add_env_to_query('{app="mysql"} |= "error"', "prod") assert result == '{app="mysql", cluster_env="prod"} |= "error"' def test_handles_empty_labels(self): """Test handling query with empty labels.""" result = add_env_to_query("{} |= error", "prod") assert result == '{cluster_env="prod"} |= error'