"""Comprehensive tests for the DB ORM module. Tests cover: - DB initialization with dataclass - Table creation and column definitions - CRUD operations (save, get, filter, update, delete) - Type mapping (Python to SQLite) - Primary key handling - Raw SQL execution with validation - Data sanitization """ import os import pytest import tempfile from dataclasses import dataclass, field from aiomql.core.db import DB @pytest.fixture def temp_db_path(): """Creates a temporary database file path.""" fd, path = tempfile.mkstemp(suffix=".db") os.close(fd) yield path if os.path.exists(path): os.remove(path) @pytest.fixture def setup_db_config(temp_db_path, monkeypatch): """Sets up DB config with temp database.""" from aiomql.core.config import Config config = Config() config.db_name = temp_db_path monkeypatch.setenv("DB_NAME", temp_db_path) yield temp_db_path @dataclass class TestModel(DB): """Test model for DB tests.""" id: int = field(metadata={"PRIMARY KEY": True}) name: str = "" value: float = 0.0 @dataclass class SimpleModel(DB): """Simple model without primary key.""" name: str = "" count: int = 0 class TestDBInitialization: """Tests for DB initialization.""" def test_dataclass_model_creates_table(self, setup_db_config): """Test dataclass model creates table on instantiation.""" record = TestModel(id=1, name="test", value=1.0) assert record is not None def test_init_sets_config(self, setup_db_config): """Test __new__ sets config.""" record = TestModel(id=1, name="test", value=1.0) assert hasattr(record, "config") def test_table_name_defaults_to_class_name(self, setup_db_config): """Test table name defaults to lowercase class name.""" record = TestModel(id=1, name="test", value=1.0) assert TestModel._table == "testmodel" class TestDBTypeMapping: """Tests for Python to SQLite type mapping.""" def test_str_maps_to_text(self): """Test str maps to TEXT.""" assert DB.types(str) == "TEXT" def test_int_maps_to_integer(self): """Test int maps to INTEGER.""" assert DB.types(int) == "INTEGER" def test_float_maps_to_real(self): """Test float maps to REAL.""" assert DB.types(float) == "REAL" def test_bool_maps_to_boolean(self): """Test bool maps to BOOLEAN.""" assert DB.types(bool) == "BOOLEAN" def test_bytes_maps_to_blob(self): """Test bytes maps to BLOB.""" assert DB.types(bytes) == "BLOB" def test_unknown_type_maps_to_text(self): """Test unknown type maps to TEXT.""" assert DB.types(list) == "TEXT" class TestDBSanitize: """Tests for SQL identifier sanitization.""" def test_valid_identifier(self): """Test valid identifier is quoted.""" result = DB.sanitize("valid_name") assert result == '"valid_name"' def test_identifier_starting_with_underscore(self): """Test identifier starting with underscore.""" result = DB.sanitize("_valid") assert result == '"_valid"' def test_invalid_identifier_raises(self): """Test invalid identifier raises ValueError.""" with pytest.raises(ValueError): DB.sanitize("invalid-name") def test_identifier_with_numbers(self): """Test identifier with numbers.""" result = DB.sanitize("name123") assert result == '"name123"' def test_identifier_starting_with_number_raises(self): """Test identifier starting with number raises.""" with pytest.raises(ValueError): DB.sanitize("123invalid") class TestDBCRUDOperations: """Tests for DB CRUD operations.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" yield def test_save_inserts_record(self, setup_db_config): """Test save inserts new record.""" record = TestModel(id=1, name="test", value=1.0) record.save() result = TestModel.get(id=1) assert result is not None assert result.name == "test" def test_get_returns_record(self, setup_db_config): """Test get returns matching record.""" record = TestModel(id=1, name="test", value=1.0) record.save() result = TestModel.get(id=1) assert result.id == 1 assert result.name == "test" def test_get_returns_none_for_no_match(self, setup_db_config): """Test get returns None for no match.""" TestModel(id=1, name="test", value=1.0) # Initialize table result = TestModel.get(id=999) assert result is None def test_filter_returns_all_matching(self, setup_db_config): """Test filter returns all matching records.""" TestModel(id=1, name="test", value=1.0).save() TestModel(id=2, name="test", value=2.0).save() TestModel(id=3, name="other", value=3.0).save() results = TestModel.filter(name="test") assert len(results) == 2 def test_filter_returns_all_when_no_criteria(self, setup_db_config): """Test filter returns all records when no criteria.""" TestModel(id=1, name="test", value=1.0).save() TestModel(id=2, name="other", value=2.0).save() results = TestModel.filter() assert len(results) == 2 def test_all_returns_all_records(self, setup_db_config): """Test all returns all records.""" TestModel(id=1, name="test1", value=1.0).save() TestModel(id=2, name="test2", value=2.0).save() results = TestModel.all() assert len(results) == 2 def test_all_with_limit(self, setup_db_config): """Test all with limit returns limited records.""" TestModel(id=1, name="test1", value=1.0).save() TestModel(id=2, name="test2", value=2.0).save() TestModel(id=3, name="test3", value=3.0).save() results = TestModel.all(limit=2) assert len(results) == 2 def test_clear_removes_all_records(self, setup_db_config): """Test clear removes all records.""" TestModel(id=1, name="test1", value=1.0).save() TestModel(id=2, name="test2", value=2.0).save() TestModel.clear() results = TestModel.all() assert len(results) == 0 def test_update_modifies_records(self, setup_db_config): """Test update modifies matching records.""" TestModel(id=1, name="old", value=1.0).save() TestModel.update({"name": "new"}, id=1) result = TestModel.get(id=1) assert result.name == "new" class TestDBPrimaryKey: """Tests for primary key handling.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" yield def test_pk_property_returns_pk_field(self, setup_db_config): """Test pk property returns primary key field name and value.""" record = TestModel(id=42, name="test", value=1.0) pk_name, pk_value = record.pk assert pk_name == "id" assert pk_value == 42 class TestDBAsDict: """Tests for asdict functionality.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" yield def test_asdict_returns_dict(self, setup_db_config): """Test asdict returns dictionary.""" record = TestModel(id=1, name="test", value=1.0) result = record.asdict() assert isinstance(result, dict) assert result["id"] == 1 assert result["name"] == "test" assert result["value"] == 1.0 class TestDBFields: """Tests for fields class method.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" yield def test_fields_returns_field_names(self, setup_db_config): """Test fields returns list of field names.""" TestModel(id=1, name="test", value=1.0) # Initialize field_names = TestModel.fields() assert "id" in field_names assert "name" in field_names assert "value" in field_names class TestDBDropTable: """Tests for drop_table functionality.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" yield def test_drop_table_removes_table(self, setup_db_config): """Test drop_table removes the table.""" record = TestModel(id=1, name="test", value=1.0) record.save() TestModel.drop_table() # Re-initializing should create fresh table TestModel._initialized = False TestModel._table = "" record2 = TestModel(id=1, name="new", value=2.0) record2.save() assert TestModel.all()[0].name == "new" class TestDBExecuteRaw: """Tests for execute_raw SQL execution.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" TestModel(id=1, name="test1", value=1.0).save() TestModel(id=2, name="test2", value=2.0).save() yield def test_execute_raw_select(self, setup_db_config): """Test execute_raw with SELECT query.""" results = TestModel.execute_raw( "SELECT * FROM testmodel WHERE id = ?", (1,) ) assert len(results) == 1 assert results[0].name == "test1" def test_execute_raw_with_named_params(self, setup_db_config): """Test execute_raw with named parameters.""" results = TestModel.execute_raw( "SELECT * FROM testmodel WHERE name = :name", {"name": "test1"} ) assert len(results) == 1 def test_execute_raw_write_requires_flag(self, setup_db_config): """Test execute_raw write operations require allow_write.""" with pytest.raises(PermissionError): TestModel.execute_raw( "UPDATE testmodel SET name = ? WHERE id = ?", ("updated", 1) ) def test_execute_raw_write_with_flag(self, setup_db_config): """Test execute_raw write with allow_write=True.""" affected = TestModel.execute_raw( "UPDATE testmodel SET name = ? WHERE id = ?", ("updated", 1), allow_write=True ) assert affected == 1 result = TestModel.get(id=1) assert result.name == "updated" def test_execute_raw_rejects_dangerous_patterns(self, setup_db_config): """Test execute_raw rejects dangerous SQL patterns.""" with pytest.raises(ValueError): TestModel.execute_raw("SELECT * FROM testmodel; DROP TABLE testmodel") def test_execute_raw_rejects_comments(self, setup_db_config): """Test execute_raw rejects SQL comments.""" with pytest.raises(ValueError): TestModel.execute_raw("SELECT * FROM testmodel -- comment") def test_execute_raw_rejects_multiple_statements(self, setup_db_config): """Test execute_raw rejects multiple statements.""" with pytest.raises(ValueError): TestModel.execute_raw("SELECT 1; SELECT 2") def test_execute_raw_empty_query_raises(self, setup_db_config): """Test execute_raw with empty query raises.""" with pytest.raises(ValueError): TestModel.execute_raw("") def test_execute_raw_invalid_params_type_raises(self, setup_db_config): """Test execute_raw with invalid params type raises.""" with pytest.raises(ValueError): TestModel.execute_raw("SELECT * FROM testmodel", "invalid") def test_execute_raw_unsupported_statement_raises(self, setup_db_config): """Test execute_raw with unsupported statement raises.""" with pytest.raises(ValueError): TestModel.execute_raw("CREATE TABLE newtable (id INTEGER)") class TestDBDictFactory: """Tests for dict_factory row conversion.""" @pytest.fixture(autouse=True) def setup_model(self, setup_db_config): """Reset model state before each test.""" TestModel._initialized = False TestModel._table = "" yield def test_dict_factory_returns_class_instances(self, setup_db_config): """Test dict_factory converts rows to class instances.""" TestModel(id=1, name="test", value=1.0).save() results = TestModel.all() assert isinstance(results[0], TestModel)