"""Test Jira alert data extraction in source_preparer.""" import json from unittest.mock import AsyncMock, MagicMock, patch import pytest from services.config import InvestibookSettings from services.source_preparer import _fetch_alert_from_jira @pytest.fixture def mock_jira_settings(): """Mock Jira settings.""" settings = MagicMock(spec=InvestibookSettings) settings.jira_base_url = "https://test.atlassian.net" settings.jira_username = "test@example.com" settings.jira_api_token = "test-token" return settings @pytest.mark.asyncio async def test_fetch_alert_from_jira_success(mock_jira_settings): """Test successful extraction of cluster_id and tenant_id from raw alert data.""" # Arrange: Mock Jira response with raw alert data raw_alert_data = { "labels": { "tidb_cluster_id": "cluster_xyz789", "o11y_tenant_id": "tenant_abc123", "alertname": "HighMemoryUsage", }, "annotations": { "description": "Memory usage exceeded 90%", }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-1956041", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-1956041") # Assert assert cluster_id == "cluster_xyz789" assert tenant_id == "tenant_abc123" # Verify API call was made correctly mock_client.get.assert_awaited_once_with( "https://test.atlassian.net/rest/api/2/issue/O11Y-1956041", auth=("test@example.com", "test-token"), timeout=30.0, ) @pytest.mark.asyncio async def test_fetch_alert_from_jira_missing_labels(mock_jira_settings): """Test extraction when labels are missing from raw alert data.""" # Arrange: Raw alert data without labels raw_alert_data = { "annotations": { "description": "Test alert", }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-9999999", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-9999999") # Assert: Should return None for missing labels assert cluster_id is None assert tenant_id is None @pytest.mark.asyncio async def test_fetch_alert_from_jira_malformed_json(mock_jira_settings): """Test extraction when raw alert JSON is malformed.""" # Arrange: Malformed JSON in customfield_10160 mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-8888888", "fields": { "customfield_10160": "not valid json {[}", }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-8888888") # Assert: Should handle gracefully and return None assert cluster_id is None assert tenant_id is None @pytest.mark.asyncio async def test_fetch_alert_from_jira_missing_customfield(mock_jira_settings): """Test extraction when customfield_10160 is missing.""" # Arrange: Missing customfield_10160 mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-7777777", "fields": {}, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-7777777") # Assert: Should return None when field is missing assert cluster_id is None assert tenant_id is None @pytest.mark.asyncio async def test_fetch_alert_from_jira_not_configured(): """Test when Jira is not configured.""" # Arrange: Settings with no Jira config settings = MagicMock(spec=InvestibookSettings) settings.jira_base_url = "" settings.jira_username = "" settings.jira_api_token = "" with patch("services.source_preparer.settings", settings): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-1234") # Assert: Should return None when not configured assert cluster_id is None assert tenant_id is None @pytest.mark.asyncio async def test_fetch_alert_from_jira_partial_labels(mock_jira_settings): """Test extraction when only one label is present.""" # Arrange: Only cluster_id in labels raw_alert_data = { "labels": { "tidb_cluster_id": "cluster_only", # o11y_tenant_id missing }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-6666666", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-6666666") # Assert: Should get cluster_id but not tenant_id assert cluster_id == "cluster_only" assert tenant_id is None @pytest.mark.asyncio async def test_fetch_alert_from_jira_fallback_to_cluster_id(mock_jira_settings): """Test fallback to cluster_id when tidb_cluster_id is missing.""" # Arrange: tidb_cluster_id missing, use cluster_id instead raw_alert_data = { "labels": { "cluster_id": "fallback_cluster_123", "o11y_tenant_id": "tenant_abc", }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-5555555", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-5555555") # Assert: Should use cluster_id as fallback assert cluster_id == "fallback_cluster_123" assert tenant_id == "tenant_abc" @pytest.mark.asyncio async def test_fetch_alert_from_jira_trim_tidb_prefix(mock_jira_settings): """Test trimming 'tidb' prefix from cluster_id.""" # Arrange: cluster_id with "tidb" prefix raw_alert_data = { "labels": { "tidb_cluster_id": "tidb123456789", "o11y_tenant_id": "tenant_xyz", }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-4444444", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-4444444") # Assert: Should trim "tidb" prefix assert cluster_id == "123456789" assert tenant_id == "tenant_xyz" @pytest.mark.asyncio async def test_fetch_alert_from_jira_no_tidb_prefix(mock_jira_settings): """Test cluster_id without 'tidb' prefix remains unchanged.""" # Arrange: cluster_id without "tidb" prefix raw_alert_data = { "labels": { "tidb_cluster_id": "987654321", "o11y_tenant_id": "tenant_123", }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-3333333", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-3333333") # Assert: Should keep cluster_id unchanged assert cluster_id == "987654321" assert tenant_id == "tenant_123" @pytest.mark.asyncio async def test_fetch_alert_from_jira_fallback_with_prefix(mock_jira_settings): """Test fallback to cluster_id with 'tidb' prefix gets trimmed.""" # Arrange: tidb_cluster_id missing, cluster_id has "tidb" prefix raw_alert_data = { "labels": { "cluster_id": "tidb999888777", "o11y_tenant_id": "tenant_def", }, } mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = { "key": "O11Y-2222222", "fields": { "customfield_10160": json.dumps(raw_alert_data), }, } mock_client = AsyncMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) with patch("services.source_preparer.settings", mock_jira_settings): with patch("httpx.AsyncClient", return_value=mock_client): # Act cluster_id, tenant_id, issue = await _fetch_alert_from_jira("O11Y-2222222") # Assert: Should fallback to cluster_id and trim prefix assert cluster_id == "999888777" assert tenant_id == "tenant_def"