"""Unit tests for ``app/domains/ai_spending/whitelist.py``. The whitelist drives bucket classification — every AI dashboard number flows through ``service_matches_ai_for_provider`` (in-memory) or ``build_service_filter_sql`` (SQL). A regression here mis-routes cost into the wrong bucket OR mis-counts cloud infrastructure as AI; either is a silent finance-grade error. So this suite tests: * Case-insensitive substring matching (Foundry whitelist matches an ``"foundry Models"`` row from a vendor with mixed casing). * Empty / blank patterns are rejected on write AND ignored on read. * Unknown provider keys on write -> 400; on read -> logged + ignored (so adding a new cloud doesn't require a whitelist write before the next aggregation run). * Length / count caps raise ValueError with a specific reason. * ``build_service_filter_sql`` produces an always-false expression for empty patterns (matches the in-memory helper's "no patterns = no rows" contract; the alternative — always-true — would silently count every cost-fact row as AI). """ from __future__ import annotations import pytest from sqlalchemy import Column, String from app.domains.ai_spending.whitelist import ( MAX_PATTERN_LENGTH, MAX_PATTERNS_PER_PROVIDER, build_service_filter_sql, service_matches_ai_for_provider, validate_whitelist_payload, ) class TestServiceMatchesAi: def test_exact_match_case_insensitive(self) -> None: assert service_matches_ai_for_provider( "Foundry Models", ["Foundry Models"], ) assert service_matches_ai_for_provider( "foundry models", ["Foundry Models"], ) def test_substring_match(self) -> None: # Azure OpenAI commonly appears as "Azure OpenAI Service" — # whitelist pattern is the shorter "Azure OpenAI". assert service_matches_ai_for_provider( "Azure OpenAI Service", ["Azure OpenAI"], ) def test_no_match(self) -> None: assert not service_matches_ai_for_provider( "Amazon S3", ["Foundry Models", "Azure OpenAI"], ) def test_empty_service_never_matches(self) -> None: assert not service_matches_ai_for_provider("", ["anything"]) assert not service_matches_ai_for_provider(None, ["anything"]) def test_empty_patterns_never_match(self) -> None: assert not service_matches_ai_for_provider("Azure OpenAI", []) def test_blank_patterns_skipped(self) -> None: # Blank / whitespace-only patterns must not match everything # (regression: ``"" in "S3"`` is True, would mis-flag S3 as AI). assert not service_matches_ai_for_provider("Amazon S3", ["", " "]) def test_first_matching_pattern_wins(self) -> None: assert service_matches_ai_for_provider( "Foundry Models", ["SageMaker", "Foundry", "OpenAI"], ) class TestValidatePayload: def test_simple_valid_payload(self) -> None: out = validate_whitelist_payload({ "azure": ["Foundry Models", "Azure OpenAI"], "aws": ["Amazon Bedrock"], }) assert out == { "azure": ["Foundry Models", "Azure OpenAI"], "aws": ["Amazon Bedrock"], } def test_keys_lowercased(self) -> None: out = validate_whitelist_payload({"Azure": ["Foundry"]}) assert out == {"azure": ["Foundry"]} def test_blank_patterns_dropped(self) -> None: out = validate_whitelist_payload({ "azure": ["Foundry", "", " ", "Azure OpenAI"], }) assert out == {"azure": ["Foundry", "Azure OpenAI"]} def test_unknown_provider_rejected(self) -> None: with pytest.raises(ValueError, match="unknown provider"): validate_whitelist_payload({"aliyun": ["Bailian"]}) def test_non_dict_rejected(self) -> None: with pytest.raises(ValueError, match="must be a JSON object"): validate_whitelist_payload(["azure", "Foundry"]) def test_non_list_value_rejected(self) -> None: with pytest.raises(ValueError, match="must be a list"): validate_whitelist_payload({"azure": "Foundry"}) def test_non_string_pattern_rejected(self) -> None: with pytest.raises(ValueError, match="must be a string"): validate_whitelist_payload({"azure": ["Foundry", 42]}) def test_pattern_too_long_rejected(self) -> None: long = "x" * (MAX_PATTERN_LENGTH + 1) with pytest.raises(ValueError, match="length must be"): validate_whitelist_payload({"azure": [long]}) def test_too_many_patterns_rejected(self) -> None: many = [f"pat{i}" for i in range(MAX_PATTERNS_PER_PROVIDER + 1)] with pytest.raises(ValueError, match="at most"): validate_whitelist_payload({"azure": many}) def test_pattern_strips_whitespace(self) -> None: out = validate_whitelist_payload({ "azure": [" Foundry ", "\tAzure OpenAI\n"], }) assert out == {"azure": ["Foundry", "Azure OpenAI"]} def test_blank_provider_key_dropped(self) -> None: out = validate_whitelist_payload({"": ["x"], "azure": ["Foundry"]}) assert out == {"azure": ["Foundry"]} class TestBuildServiceFilterSql: """We compile the expression to a textual SQL string to assert shape — running against an actual DB would test integration, not the helper's contract. The contract is: every non-blank pattern produces one ILIKE clause; empty pattern list -> always-false.""" def test_single_pattern(self) -> None: col = Column("service", String) expr = build_service_filter_sql(col, ["Foundry"]) compiled = str(expr.compile(compile_kwargs={"literal_binds": True})) # SQLAlchemy's default dialect renders ILIKE as # ``LOWER(col) LIKE LOWER(pattern)`` — both forms preserve # case-insensitive substring semantics. upper = compiled.upper() assert "LIKE" in upper assert "%Foundry%" in compiled or "%foundry%" in compiled.lower() def test_multiple_patterns_or_joined(self) -> None: col = Column("service", String) expr = build_service_filter_sql(col, ["Foundry", "Azure OpenAI"]) compiled = str(expr.compile(compile_kwargs={"literal_binds": True})) lower = compiled.lower() # Both patterns appear in the compiled SQL. assert "%foundry%" in lower assert "%azure openai%" in lower # Joined by OR (this is the bucket filter). assert " OR " in compiled.upper() def test_empty_patterns_always_false(self) -> None: col = Column("service", String) expr = build_service_filter_sql(col, []) compiled = str(expr.compile(compile_kwargs={"literal_binds": True})) # ``column IS NULL AND column IS NOT NULL`` is the always-false # sentinel. Confirms we don't return ``True`` (which would # silently include every row as AI). upper = compiled.upper() assert "IS NULL" in upper assert "IS NOT NULL" in upper def test_blank_patterns_treated_as_empty(self) -> None: col = Column("service", String) expr = build_service_filter_sql(col, ["", " "]) compiled = str(expr.compile(compile_kwargs={"literal_binds": True})) upper = compiled.upper() # No LIKE clause — falls through to always-false. assert "LIKE" not in upper assert "IS NULL" in upper