"""Tests for datadog_utils.py.""" from unittest.mock import MagicMock import pytest from common.datadog_utils import ( list_frontends_with_details, list_services_with_details, update_scorecard_non_service_outcomes, update_scorecard_service_outcomes_batch, ) class MockResponse: """Mock response object for API calls.""" def __init__(self, data=None, included=None): """Initialize mock response.""" self.data = data or [] self.included = included or [] @pytest.fixture def mock_api_client(mocker): """Mock Datadog ApiClient.""" return mocker.patch("common.datadog_utils.ApiClient") @pytest.fixture def mock_software_catalog_api(mocker): """Mock SoftwareCatalogApi and return the instance.""" m_api_class = mocker.patch("common.datadog_utils.SoftwareCatalogApi") mock_instance = MagicMock() m_api_class.return_value = mock_instance return mock_instance @pytest.fixture def mock_scorecards_api(mocker): """Mock ServiceScorecardsApi and return the instance.""" m_api_class = mocker.patch("common.datadog_utils.ScorecardsApi") mock_instance = MagicMock() m_api_class.return_value = mock_instance return mock_instance @pytest.fixture def sample_service_item(): """Create a sample service entity item with proper nested structure.""" item = MagicMock() item.type = "schema" # Create attributes mock with schema property attributes_mock = MagicMock() attributes_mock.schema = { "metadata": {"name": "service-a", "tags": ["language_version:1.0"]}, "spec": {"languages": ["python"]}, "datadog": { "codeLocations": [ {"repositoryURL": "https://github.com/org/repo.git"} ] }, } item.attributes = attributes_mock return item @pytest.fixture def sample_frontend_item(): """Create a sample frontend entity item with proper nested structure.""" item = MagicMock() item.type = "schema" # Create attributes mock with schema property attributes_mock = MagicMock() attributes_mock.schema = { "metadata": { "name": "frontend-a", "tags": ["language:js", "language_version:1.0"], }, "spec": {"type": "browser"}, "datadog": { "codeLocations": [ {"repositoryURL": "https://github.com/org/frontend.git"} ] }, } item.attributes = attributes_mock return item @pytest.fixture def sample_outcomes(): """Create sample outcomes for batch testing.""" return [ { "service_name": f"svc-{service}", "rule_id": "rule-1", "state": "PASS", "remarks": "ok", } for service in range(150) ] @pytest.fixture def sample_non_service_outcomes(): """Create sample non-service outcomes for batch testing.""" return [ { "entity_reference": f"frontend:frontend-{idx}", "rule_id": "rule-1", "state": "PASS", "remarks": "ok", } for idx in range(150) ] def test_list_services_with_details( mock_software_catalog_api, sample_service_item ): """Test listing services with details from the software catalog. Verifies that: - Services are correctly parsed from the API response - Service name, languages, and tags are extracted properly - Pagination works correctly across multiple pages - Invalid/missing data is handled gracefully """ """ Mock return values for pagination (2 pages), including one service item with metadata and without. This should result in a single valid service being returned. """ page1 = MockResponse(data=["ref1"], included=[sample_service_item]) page2 = MockResponse(data=[], included=[]) mock_software_catalog_api.list_catalog_entity.side_effect = [page1, page2] services = list_services_with_details("fake-api", "fake-app") assert len(services) == 1 assert services[0]["name"] == "service-a" assert services[0]["languages"] == ["python"] assert services[0]["tags"] == ["language_version:1.0"] assert services[0]["raw_schema"] == { "datadog": { "codeLocations": [ {"repositoryURL": "https://github.com/org/repo.git"} ] } } def test_list_frontends_with_details( mock_software_catalog_api, sample_frontend_item ): """Test listing frontends with details from the software catalog. Verifies that: - Frontends are correctly parsed from the API response - Frontend name, languages, and tags are extracted properly - Pagination works correctly across multiple pages - Invalid/missing data is handled gracefully """ """ Mock return values for pagination (2 pages), including one frontend item with metadata and without. This should result in a single valid frontend being returned. """ page1 = MockResponse(data=["ref1"], included=[sample_frontend_item]) page2 = MockResponse(data=[], included=[]) mock_software_catalog_api.list_catalog_entity.side_effect = [page1, page2] frontends = list_frontends_with_details("fake-api", "fake-app") assert len(frontends) == 1 assert frontends[0]["name"] == "frontend-a" assert frontends[0]["languages"] == ["js"] assert frontends[0]["tags"] == ["language:js", "language_version:1.0"] assert frontends[0]["raw_schema"] == { "datadog": { "codeLocations": [ {"repositoryURL": "https://github.com/org/frontend.git"} ] } } def test_list_services_raw_schema_no_code_locations(mock_software_catalog_api): """Service without a datadog section returns an empty raw_schema.""" item = MagicMock() item.type = "schema" attributes_mock = MagicMock() attributes_mock.schema = { "metadata": {"name": "bare-service", "tags": []}, "spec": {"languages": []}, } item.attributes = attributes_mock mock_software_catalog_api.list_catalog_entity.side_effect = [ MockResponse(data=["ref1"], included=[item]), MockResponse(data=[], included=[]), ] services = list_services_with_details("fake-api", "fake-app") assert len(services) == 1 assert services[0]["raw_schema"] == {"datadog": {}} def test_list_frontends_raw_schema_no_code_locations(mock_software_catalog_api): """Frontend without a datadog section returns an empty raw_schema.""" item = MagicMock() item.type = "schema" attributes_mock = MagicMock() attributes_mock.schema = { "metadata": {"name": "bare-frontend", "tags": []}, "spec": {"type": "browser"}, } item.attributes = attributes_mock mock_software_catalog_api.list_catalog_entity.side_effect = [ MockResponse(data=["ref1"], included=[item]), MockResponse(data=[], included=[]), ] frontends = list_frontends_with_details("fake-api", "fake-app") assert len(frontends) == 1 assert frontends[0]["raw_schema"] == {"datadog": {}} def test_update_scorecard_outcomes_batch(mock_scorecards_api, sample_outcomes): """Test batch updating of scorecard outcomes. Verifies that: - Outcomes are batched correctly (100 items per batch) - Multiple batches are created when needed - The API is called the correct number of times - Batch sizes are correct for first and subsequent batches """ update_scorecard_service_outcomes_batch( "fake-api", "fake-app", sample_outcomes ) # Should be called 2 times assert mock_scorecards_api.create_scorecard_outcomes_batch.call_count == 2 # Verify first batch size ( _, args, ) = mock_scorecards_api.create_scorecard_outcomes_batch.call_args_list[0] assert len(args["body"]["data"]["attributes"]["results"]) == 100 for result in args["body"]["data"]["attributes"]["results"]: assert result["service_name"] == sample_outcomes.pop(0)["service_name"] # Verify second batch size ( _, args, ) = mock_scorecards_api.create_scorecard_outcomes_batch.call_args_list[1] assert len(args["body"]["data"]["attributes"]["results"]) == 50 for result in args["body"]["data"]["attributes"]["results"]: assert result["service_name"] == sample_outcomes.pop(0)["service_name"] def test_update_scorecard_service_outcomes_missing_remarks( mock_scorecards_api, ): """Test service outcomes omit remarks when not provided.""" update_scorecard_service_outcomes_batch( "fake-api", "fake-app", [ { "service_name": "svc-1", "rule_id": "rule-1", "state": "PASS", } ], ) ( _, args, ) = mock_scorecards_api.create_scorecard_outcomes_batch.call_args_list[0] results = args["body"]["data"]["attributes"]["results"] assert len(results) == 1 assert results[0]["service_name"] == "svc-1" assert "remarks" not in results[0] def test_update_scorecard_non_service_outcomes_batch( mock_api_client, mock_scorecards_api, sample_non_service_outcomes, ): """Test batch updating scorecard outcomes for non-service entities.""" update_scorecard_non_service_outcomes( "fake-api", "fake-app", sample_non_service_outcomes ) assert mock_scorecards_api.update_scorecard_outcomes.call_count == 2 (_, args) = mock_scorecards_api.update_scorecard_outcomes.call_args_list[0] assert len(args["body"]["data"]["attributes"]["results"]) == 100 for result in args["body"]["data"]["attributes"]["results"]: assert ( result["entity_reference"] == sample_non_service_outcomes.pop(0)["entity_reference"] ) (_, args) = mock_scorecards_api.update_scorecard_outcomes.call_args_list[1] assert len(args["body"]["data"]["attributes"]["results"]) == 50 for result in args["body"]["data"]["attributes"]["results"]: assert ( result["entity_reference"] == sample_non_service_outcomes.pop(0)["entity_reference"] ) def test_update_scorecard_non_service_outcomes_missing_remarks( mock_scorecards_api, ): """Test non-service outcomes set remarks to unset when omitted.""" update_scorecard_non_service_outcomes( "fake-api", "fake-app", [ { "entity_reference": "frontend:frontend-1", "rule_id": "rule-1", "state": "PASS", } ], ) (_, args) = mock_scorecards_api.update_scorecard_outcomes.call_args_list[0] results = args["body"]["data"]["attributes"]["results"] assert len(results) == 1 assert results[0]["entity_reference"] == "frontend:frontend-1" assert "remarks" not in results[0] def test_update_scorecard_non_service_outcomes_no_outcomes(mock_scorecards_api): """Test non-service outcomes update short-circuits when input is empty.""" update_scorecard_non_service_outcomes("fake-api", "fake-app", []) mock_scorecards_api.update_scorecard_outcomes.assert_not_called()