agnes-the-ai-analyst/tests/test_sql_safe.py
ZdenekSrotyr 2e1dfb7553
feat(v2): claude-driven fetch primitives + 0.14.0 (#102)
Replaces the BigQuery wrap-view pattern with a discovery + scoped-fetch toolkit driven by the analyst's Claude session. Adds /api/v2/{catalog,schema,sample,scan,scan/estimate}, da catalog/schema/describe/fetch/snapshot/disk-info CLI commands, sqlglot-backed WHERE validator, process-local quota tracker, agent rails skill (cli/skills/agnes-data-querying.md). BREAKING: BQ wrap views off by default — set data_source.bigquery.legacy_wrap_views=true for one cycle. Backward-compat field_validator on primary_key. Catalog cache now matches documented 300s TTL with RBAC fresh per request. Cuts release v0.14.0.
2026-04-29 01:07:19 +02:00

104 lines
3.3 KiB
Python

"""Tests for src.sql_safe — identifier and project_id validators."""
import pytest
from src.sql_safe import (
is_safe_identifier,
is_safe_project_id,
validate_identifier,
validate_project_id,
)
class TestIsSafeIdentifier:
@pytest.mark.parametrize("name", ["orders", "T_1", "_x", "a" * 64])
def test_accepts_valid_identifiers(self, name):
assert is_safe_identifier(name) is True
@pytest.mark.parametrize(
"name",
[
"",
"1leading_digit",
"has space",
"has-dash",
"has.dot",
"has;semicolon",
"has'quote",
"has\"doublequote",
"has`backtick",
"a" * 65, # too long
],
)
def test_rejects_unsafe_identifiers(self, name):
assert is_safe_identifier(name) is False
def test_rejects_non_string(self):
assert is_safe_identifier(None) is False
assert is_safe_identifier(123) is False
class TestIsSafeProjectId:
@pytest.mark.parametrize(
"pid",
[
"abcdef", # 6 chars (minimum)
"my-project", # standard form
"abc12-345", # mid hyphen + digits
"a" + "b" * 28 + "c", # 30 chars (maximum)
],
)
def test_accepts_valid_project_ids(self, pid):
assert is_safe_project_id(pid) is True
@pytest.mark.parametrize(
"pid",
[
"",
"abc", # too short
"ABC123", # uppercase rejected
"1leading-digit", # must start with letter
"trailing-", # cannot end with hyphen
"has_underscore", # underscore not allowed
"a" * 31, # too long
"has space",
"has.dot",
"has;semicolon",
"has'quote",
"evil'; DROP TABLE foo; --",
],
)
def test_rejects_unsafe_project_ids(self, pid):
assert is_safe_project_id(pid) is False
def test_rejects_non_string(self):
assert is_safe_project_id(None) is False
assert is_safe_project_id(42) is False
class TestValidateIdentifier:
def test_returns_true_and_no_warning_on_valid(self, caplog):
import logging
with caplog.at_level(logging.WARNING):
assert validate_identifier("orders", "table_name") is True
assert caplog.records == []
def test_returns_false_and_warns_on_invalid(self, caplog):
import logging
with caplog.at_level(logging.WARNING, logger="src.sql_safe"):
assert validate_identifier("evil; DROP", "table_name") is False
assert any("table_name" in r.message for r in caplog.records)
class TestValidateProjectId:
def test_returns_true_and_no_warning_on_valid(self, caplog):
import logging
with caplog.at_level(logging.WARNING):
assert validate_project_id("my-project") is True
assert caplog.records == []
def test_returns_false_and_warns_on_invalid(self, caplog):
import logging
with caplog.at_level(logging.WARNING, logger="src.sql_safe"):
assert validate_project_id("evil'; DROP TABLE foo; --") is False
assert any("project_id" in r.message for r in caplog.records)