"""Tests for the hardening observability helpers. The key guardrail: metric tags carry the key TYPE, never the key VALUE (PII/cardinality). The metric names and the breaker 0-then-1 emit sequence are the wire contract with dashboards and monitors, so they are pinned too. """ from typing import Any from unittest.mock import patch import pytest from core.hardening import observability as obs _ALLOWED_TAG_KEYS = { 'category', 'key_type', 'resource', 'state', 'outcome', 'endpoint', 'entity', 'route', } def _capture(monkeypatch: pytest.MonkeyPatch) -> list[tuple[Any, ...]]: """Capture (metric, tags) / (metric, value, tags) tuples instead of emitting to statsd.""" sent: list[tuple[Any, ...]] = [] def count(metric: str, tags: dict[str, str]) -> None: sent.append((metric, tags)) def count_by(metric: str, value: int, tags: dict[str, str]) -> None: sent.append((metric, value, tags)) def gauge(metric: str, value: float, tags: dict[str, str]) -> None: sent.append((metric, value, tags)) def distribution(metric: str, value: float, tags: dict[str, str]) -> None: sent.append((metric, value, tags)) monkeypatch.setattr(obs, '_count', count) monkeypatch.setattr(obs, '_count_by', count_by) monkeypatch.setattr(obs, '_gauge', gauge) monkeypatch.setattr(obs, '_distribution', distribution) return sent def test_tags_are_key_type_not_value(monkeypatch: pytest.MonkeyPatch) -> None: """Every emitted tag key is a key TYPE from the allow-list, never the key value.""" sent = _capture(monkeypatch) obs.rate_limit_rejected(category='expensive', key_type='ip') obs.on_breaker_state('snowflake', 'closed', 'open') obs.request_body_bytes(1234, endpoint='contract_api.create_contract') obs.dataloader_batch( 'ContractTerm', route='/contract-term/dataloader', requested=200, unresolved=3 ) obs.dataloader_key_field_mismatch('ContractTerm', route='/contract-term/dataloader') for emit in sent: tags = emit[-1] assert set(tags).issubset(_ALLOWED_TAG_KEYS) # never the key VALUE def test_request_body_bytes_is_a_distribution_tagged_by_endpoint( monkeypatch: pytest.MonkeyPatch, ) -> None: """Observed body size is a distribution metric carrying only the (bounded) endpoint name.""" sent = _capture(monkeypatch) obs.request_body_bytes(2048, endpoint='contract_api.create_contract') assert sent == [ ( 'hardening.request.body_bytes', 2048, {'endpoint': 'contract_api.create_contract'}, ), ] def test_counters_use_stable_metric_names_and_tag_shapes( monkeypatch: pytest.MonkeyPatch, ) -> None: """The counter metric names and tag shapes are the dashboard contract.""" sent = _capture(monkeypatch) obs.rate_limit_rejected(category='read', key_type='principal') obs.body_too_large() obs.downstream_failure(resource='snowflake') assert sent == [ ('hardening.ratelimit.rejected', {'category': 'read', 'key_type': 'principal'}), ('hardening.body_limit.rejected', {}), ('hardening.downstream.failure', {'resource': 'snowflake'}), ] def test_on_breaker_state_clears_old_then_sets_new( monkeypatch: pytest.MonkeyPatch, ) -> None: """A transition emits gauge 0 for the old state then gauge 1 for the new, in that order.""" sent = _capture(monkeypatch) obs.on_breaker_state('snowflake', 'closed', 'open') assert sent == [ ('hardening.breaker.state', 0, {'resource': 'snowflake', 'state': 'closed'}), ('hardening.breaker.state', 1, {'resource': 'snowflake', 'state': 'open'}), ] def test_dataloader_batch_distribution_for_requested_counter_for_unresolved() -> None: """requested_ids is a distribution (batch-size percentiles); unresolved_ids a counter (sum).""" with ( patch.object(obs, '_distribution') as distribution, patch.object(obs, '_count_by') as count_by, ): obs.dataloader_batch( 'ContractTerm', route='/contract-term/dataloader', requested=200, unresolved=3, ) distribution.assert_called_once_with( 'dataloader.requested_ids', 200, {'entity': 'ContractTerm', 'route': '/contract-term/dataloader'}, ) count_by.assert_called_once_with( 'dataloader.unresolved_ids', 3, {'entity': 'ContractTerm', 'route': '/contract-term/dataloader'}, ) def test_dataloader_key_field_mismatch_increments_tagged_counter( monkeypatch: pytest.MonkeyPatch, ) -> None: """dataloader_key_field_mismatch counts the mismatch, tagged by entity TYPE and route.""" sent = _capture(monkeypatch) obs.dataloader_key_field_mismatch('ContractTerm', route='/contract-term/dataloader') assert sent == [ ( 'dataloader.key_field_mismatch', {'entity': 'ContractTerm', 'route': '/contract-term/dataloader'}, ) ] def test_ratelimit_fallback_increments_tagged_counter() -> None: """ratelimit_fallback increments hardening.ratelimit.fallback tagged by reason.""" with patch.object(obs, '_statsd') as statsd: obs.ratelimit_fallback('redis_error') statsd.increment.assert_called_once_with( 'hardening.ratelimit.fallback', tags=['reason:redis_error'] )