Files
GTI_GERENCIADOR_FISCAL/tests/test_collector.py
T

292 lines
12 KiB
Python

import pytest
from gti_discovery.catalog import FirebirdCatalog
from gti_discovery.firebird_queries import _ADAPTER_REGISTRATION_TOKEN, _register_approved_adapter
from gti_discovery.collector import ReadOnlyCatalogReader, _CATALOG_QUERIES, _FIXTURE_REGISTRATION_TOKEN, _approved_fixture_reader, _fetch_rows, _is_optional_unsupported, _safe_value, collect_metadata
from gti_discovery.fingerprint import build_fingerprint
from gti_discovery.read_only import validate_read_only
@_approved_fixture_reader(token=_FIXTURE_REGISTRATION_TOKEN)
class CatalogFixture(ReadOnlyCatalogReader):
def __init__(self, failures=None, missing=None):
self.failures = failures or {}
self.missing = missing or set()
self.calls = []
def _rows(self, name):
self.calls.append(name)
if name in self.failures:
raise self.failures[name]
return [{"NAME": name, "POSITION": 0}]
def __getattribute__(self, name):
if name.startswith("list_") and name.removeprefix("list_") in object.__getattribute__(self, "missing"):
raise AttributeError(name)
return object.__getattribute__(self, name)
def list_database(self): return self._rows("database")
def list_relations(self): return self._rows("relations")
def list_relation_fields(self): return self._rows("fields")
def list_keys(self): return self._rows("keys")
def list_indexes(self): return self._rows("indexes")
def list_foreign_keys(self): return self._rows("foreign_keys")
def list_triggers(self): return self._rows("triggers")
def list_procedures(self): return self._rows("procedures")
def list_procedure_parameters(self): return self._rows("procedure_parameters")
def list_packages(self): return self._rows("packages")
def list_generators(self): return self._rows("generators")
def list_dependencies(self): return self._rows("dependencies")
def list_monitoring(self): return self._rows("monitoring")
def test_optional_catalogs_are_collected_independently():
metadata = collect_metadata(CatalogFixture())
assert metadata["generators"] == [{"NAME": "generators", "POSITION": 0}]
assert metadata["dependencies"] == [{"NAME": "dependencies", "POSITION": 0}]
assert metadata["monitoring"] == [{"NAME": "monitoring", "POSITION": 0}]
def test_optional_unsupported_catalog_is_unavailable_but_permission_is_an_error():
reader = CatalogFixture(
{
"packages": RuntimeError("Table unknown RDB$PACKAGES"),
"generators": RuntimeError("no permission for SELECT"),
}
)
metadata = collect_metadata(reader)
assert "packages" in metadata["unavailable"]
assert {error["kind"] for error in metadata["errors"]} == {"optional_unsupported", "query_error"}
assert {error["query"] for error in metadata["errors"]} == {"generators", "packages"}
@pytest.mark.parametrize("message", [
"permission denied: catalog unsupported",
"insufficient privilege; feature not supported",
"access denied: table unknown",
])
def test_permission_failures_never_count_as_optional_unsupported(message):
assert not _is_optional_unsupported("packages", RuntimeError(message))
@pytest.mark.parametrize("error", [PermissionError(), PermissionError("catalog unsupported")])
def test_permission_error_type_is_never_optional_unsupported(error):
assert not _is_optional_unsupported("packages", error)
def test_empty_permission_error_is_recorded_as_query_error():
metadata = collect_metadata(CatalogFixture({"packages": PermissionError()}))
assert metadata["errors"] == [{"query": "packages", "kind": "query_error", "error": "PermissionError"}]
def test_missing_optional_reader_method_does_not_raise_during_collection():
metadata = collect_metadata(CatalogFixture(missing={"packages"}))
assert "packages" in metadata["unavailable"]
def test_firebird_catalog_implements_closed_interface_explicitly():
class Adapter:
read_only = True
def execute(self, sql, params):
return []
_register_approved_adapter(Adapter, _ADAPTER_REGISTRATION_TOKEN)
reader = FirebirdCatalog(Adapter())
assert isinstance(reader, ReadOnlyCatalogReader)
assert reader.list_packages() == []
def test_firebird_catalog_exposes_no_arbitrary_sql_method():
"""Mission 00D: the closed API must never offer execute/query/raw_execute."""
class Adapter:
read_only = True
def execute(self, sql, params):
return []
_register_approved_adapter(Adapter, _ADAPTER_REGISTRATION_TOKEN)
reader = FirebirdCatalog(Adapter())
for forbidden in ("execute", "query", "raw_execute", "execute_statement", "run_sql"):
assert not hasattr(reader, forbidden), f"FirebirdCatalog must not expose {forbidden!r}"
def test_firebird_catalog_rejects_adapter_without_proven_read_only():
class Unproven:
read_only = False
def execute(self, sql, params):
return []
_register_approved_adapter(Unproven, _ADAPTER_REGISTRATION_TOKEN)
with pytest.raises(ValueError):
FirebirdCatalog(Unproven())
def test_firebird_catalog_hides_raw_handle_from_attribute_access():
class Adapter:
read_only = True
def execute(self, sql, params):
return []
_register_approved_adapter(Adapter, _ADAPTER_REGISTRATION_TOKEN)
reader = FirebirdCatalog(Adapter())
with pytest.raises(AttributeError):
reader._handle
@pytest.mark.parametrize("failure", [NotImplementedError(), RuntimeError("catalog unsupported")])
def test_explicit_optional_capability_failures_are_unavailable(failure):
metadata = collect_metadata(CatalogFixture({"monitoring": failure}))
assert "monitoring" in metadata["unavailable"]
assert metadata["errors"][0]["kind"] == "optional_unsupported"
def test_tuple_row_values_are_sanitized_field_by_field():
class UnknownValue:
pass
row = ("PRODUCT", 0, b"binary", "password=hidden", "x" * 2048, UnknownValue())
assert _safe_value(row) == ["PRODUCT", 0, "<redacted>", "<redacted>", "<redacted>", "<redacted>"]
def test_db_api_tuple_rows_use_cursor_description_for_metadata_names():
class Cursor:
description = [("RELATION_NAME",), ("FIELD_POSITION",)]
def fetchall(self):
return [("PRODUCT", 0)]
assert _fetch_rows(Cursor()) == [{"RELATION_NAME": "PRODUCT", "FIELD_POSITION": 0}]
def test_all_catalog_queries_are_select_only_and_source_free():
for query in _CATALOG_QUERIES.values():
validate_read_only(query)
assert not any(token in query.upper() for token in ("RDB$TRIGGER_SOURCE", "RDB$PROCEDURE_SOURCE", "RDB$PACKAGE_HEADER_SOURCE", "RDB$PACKAGE_BODY_SOURCE", "RDB$DEFAULT_SOURCE"))
def test_missing_optional_reader_is_recorded_as_unavailable():
reader = CatalogFixture(missing={"packages"})
metadata = collect_metadata(reader)
assert "packages" in metadata["unavailable"]
def test_collect_metadata_rejects_duck_typed_non_reader():
"""Mission 00D Phase 9: collect_metadata must not accept an
arbitrary object just because it happens to expose the same method
names -- ReadOnlyCatalogReader is an ABC and this is a runtime
check (isinstance), not merely a type hint a caller could ignore."""
class DuckTyped:
def get_database_info(self): return []
def list_relations(self): return []
def list_relation_fields(self): return []
def list_keys(self): return []
def list_indexes(self): return []
def list_foreign_keys(self): return []
def list_triggers(self): return []
def list_procedures(self): return []
def list_procedure_parameters(self): return []
def list_packages(self): return []
with pytest.raises(TypeError, match="reader must be ReadOnlyCatalogReader"):
collect_metadata(DuckTyped())
def test_collect_metadata_accepts_real_firebird_catalog():
class Adapter:
read_only = True
def execute(self, sql, params=()):
return []
_register_approved_adapter(Adapter, _ADAPTER_REGISTRATION_TOKEN)
metadata = collect_metadata(FirebirdCatalog(Adapter()))
assert metadata["database"] == []
def test_collection_row_order_is_deterministic_for_reversed_reader_rows():
@_approved_fixture_reader(token=_FIXTURE_REGISTRATION_TOKEN)
class ReorderedReader(ReadOnlyCatalogReader):
def __init__(self, reverse):
self.reverse = reverse
def _rows(self, name):
rows = {
"database": [{"MON$DATABASE_NAME": "DB", "MON$ODS_MAJOR": 13}],
"relations": [{"RELATION_NAME": "B", "RELATION_ID": 2}, {"RELATION_NAME": "A", "RELATION_ID": 1}],
"fields": [
{"RELATION_NAME": "B", "FIELD_NAME": "Z", "FIELD_POSITION": 1, "TYPE_NAME": "INTEGER"},
{"RELATION_NAME": "A", "FIELD_NAME": "ID", "FIELD_POSITION": 0, "TYPE_NAME": "INTEGER"},
],
"keys": [{"RELATION_NAME": "B", "CONSTRAINT_TYPE": "PRIMARY KEY", "FIELD_NAME": "ID"}],
"indexes": [{"RELATION_NAME": "A", "INDEX_NAME": "IX_A", "FIELD_NAME": "ID"}],
"foreign_keys": [{"CHILD_RELATION_NAME": "B", "REFERENCED_RELATION_NAME": "A", "CHILD_FIELD_NAME": "A_ID"}],
"triggers": [{"TRIGGER_NAME": "TR_B", "RELATION_NAME": "B", "TRIGGER_TYPE": 1}],
"procedures": [{"PROCEDURE_NAME": "P_B", "PROCEDURE_ID": 2}, {"PROCEDURE_NAME": "P_A", "PROCEDURE_ID": 1}],
"procedure_parameters": [{"PROCEDURE_NAME": "P_B", "PARAMETER_NUMBER": 1}],
"packages": [{"PACKAGE_NAME": "PKG_B", "PACKAGE_ID": 2}],
"generators": [{"GENERATOR_NAME": "GEN_B", "GENERATOR_ID": 2}],
"dependencies": [{"DEPENDENT_NAME": "P_B", "DEPENDED_ON_NAME": "B"}],
"monitoring": [(2, "running"), (1, "idle")],
}[name]
return list(reversed(rows)) if self.reverse else rows
def list_database(self): return self._rows("database")
def list_relations(self): return self._rows("relations")
def list_relation_fields(self): return self._rows("fields")
def list_keys(self): return self._rows("keys")
def list_indexes(self): return self._rows("indexes")
def list_foreign_keys(self): return self._rows("foreign_keys")
def list_triggers(self): return self._rows("triggers")
def list_procedures(self): return self._rows("procedures")
def list_procedure_parameters(self): return self._rows("procedure_parameters")
def list_packages(self): return self._rows("packages")
def list_generators(self): return self._rows("generators")
def list_dependencies(self): return self._rows("dependencies")
def list_monitoring(self): return self._rows("monitoring")
assert collect_metadata(ReorderedReader(False)) == collect_metadata(ReorderedReader(True))
def test_collect_metadata_rejects_structured_operation_overrides():
class OverriddenReader(ReadOnlyCatalogReader):
def get_database_info(self):
return []
with pytest.raises(TypeError, match="sealed structured operations"):
collect_metadata(OverriddenReader())
def test_approved_fixture_registration_does_not_authorize_subclasses():
class DerivedFixture(CatalogFixture):
pass
with pytest.raises(TypeError, match="sealed structured operations"):
collect_metadata(DerivedFixture())
def test_catalog_query_map_is_not_exported_from_collector():
import gti_discovery.collector as collector
assert not hasattr(collector, "METADATA_QUERIES")
def test_engine_context_changes_fingerprint():
base = {"database": [{"ODS_MAJOR": 12}], "engine": {"dialect": 3}}
changed = {"database": [{"ODS_MAJOR": 12}], "engine": {"dialect": 1}}
assert build_fingerprint(base) != build_fingerprint(changed)