"""Unit tests for log aggregation helper.""" from types import SimpleNamespace import pytest from datadog_api_client import Configuration from pytest_mock import MockerFixture from src.aggregate_logs import AggregateLogsConfig, aggregate_logs def _bucket( by: object | None, computes: object | None, ) -> SimpleNamespace: """Create a bucket-like object returned by the Datadog client.""" return SimpleNamespace(by=by, computes=computes) def _response(data: SimpleNamespace | None) -> SimpleNamespace: """Create a response-like object returned by the Datadog client.""" return SimpleNamespace(data=data) FACET = "@custom-facet" @pytest.fixture def mock_dd_configuration( api_key: str = "api-key", app_key: str = "app-key", ) -> Configuration: """Create a Datadog Configuration object for tests.""" return Configuration( api_key={ "apiKeyAuth": api_key, "appKeyAuth": app_key, } ) @pytest.fixture def aggregate_logs_config() -> AggregateLogsConfig: """Create an AggregateLogsConfig for tests.""" return AggregateLogsConfig( query="service:custom", facet=FACET, group_by_limit=25, logs_from="now-2h", logs_to="now", ) def test_aggregate_logs_returns_grouped_computes( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Returns compute values keyed by valid facet values.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response( SimpleNamespace( buckets=[ _bucket(by={FACET: "ios-app"}, computes={"count": 3}), _bucket(by={FACET: "android-app"}, computes=None), ] ) ) result = aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) assert result == { "ios-app": {"count": 3}, "android-app": {}, } logs_api.aggregate_logs.assert_called_once() @pytest.mark.parametrize( "response_data", [ pytest.param(None, id="no-data"), pytest.param(SimpleNamespace(buckets=None), id="no-buckets"), ], ) def test_aggregate_logs_handles_empty_payload( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, response_data: SimpleNamespace | None, ) -> None: """Returns an empty mapping when response data or buckets are missing.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response(response_data) result = aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) assert result == {} def test_aggregate_logs_skips_invalid_or_blank_facet_values( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Skips buckets where the facet value is missing, blank, or non-string.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response( SimpleNamespace( buckets=[ _bucket(by=None, computes={"count": 1}), _bucket(by={FACET: ""}, computes={"count": 2}), _bucket(by={FACET: " "}, computes={"count": 3}), _bucket(by={FACET: 99}, computes={"count": 4}), _bucket(by={FACET: "web"}, computes={"count": 5}), ] ) ) result = aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) assert result == {"web": {"count": 5}} def test_aggregate_logs_duplicate_facet_values_keep_latest_bucket( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Uses the latest bucket when a facet value appears more than once.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response( SimpleNamespace( buckets=[ _bucket(by={FACET: "web"}, computes={"count": 1}), _bucket(by={FACET: "web"}, computes={"count": 4}), ] ) ) result = aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) assert result == {"web": {"count": 4}} def test_aggregate_logs_raises_when_bucket_by_is_not_mapping( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Raises if Datadog returns a truthy non-mapping bucket.by payload.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response( SimpleNamespace( buckets=[ _bucket(by=[("not", "a dict")], computes={"count": 1}), ] ) ) with pytest.raises(AttributeError): aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) def test_aggregate_logs_preserves_truthy_non_dict_computes( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Stores non-dict truthy computes values as-is in the output mapping.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response( SimpleNamespace( buckets=[ _bucket(by={FACET: "web"}, computes=["not", "a", "dict"]), ] ) ) result = aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) assert result == {"web": ["not", "a", "dict"]} def test_aggregate_logs_propagates_api_errors( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Does not swallow exceptions raised by the Datadog API client.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.side_effect = RuntimeError("datadog unavailable") with pytest.raises(RuntimeError, match="datadog unavailable"): aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) def test_aggregate_logs_builds_expected_request_auth( aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Builds Datadog request with expected auth and query configuration.""" provided_configuration = Configuration( api_key={ "apiKeyAuth": "my-api-key", "appKeyAuth": "my-app-key", } ) api_client = mocker.MagicMock() api_client.__enter__.return_value = object() api_client_cls = mocker.patch( "src.aggregate_logs.ApiClient", return_value=api_client, ) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response(SimpleNamespace(buckets=[])) aggregate_logs( configuration=provided_configuration, aggregate_config=aggregate_logs_config, ) api_client_cls.assert_called_once_with(provided_configuration) def test_aggregate_logs_uses_passed_config_values( mock_dd_configuration: Configuration, aggregate_logs_config: AggregateLogsConfig, mocker: MockerFixture, ) -> None: """Uses caller-provided config for filtering, grouping, and facet lookup.""" api_client = mocker.MagicMock() api_client.__enter__.return_value = object() mocker.patch("src.aggregate_logs.ApiClient", return_value=api_client) logs_api = mocker.patch("src.aggregate_logs.LogsApi").return_value logs_api.aggregate_logs.return_value = _response( SimpleNamespace( buckets=[ _bucket( by={FACET: "service-a"}, computes={"count": 11}, ), ] ) ) result = aggregate_logs( configuration=mock_dd_configuration, aggregate_config=aggregate_logs_config, ) assert result == {"service-a": {"count": 11}} request_body = logs_api.aggregate_logs.call_args.kwargs["body"] assert request_body.filter.query == "service:custom" assert request_body.filter._from == "now-2h" assert request_body.filter.to == "now" assert request_body.group_by[0].facet == "@custom-facet" assert request_body.group_by[0].limit == 25