# ABOUTME: Tests for the reference tables migration (0001-reference-tables.sql). # ABOUTME: Validates that tables are created with correct schema and constraints. import apsw import pytest from animaltrack.db import get_db from animaltrack.migrations import run_migrations @pytest.fixture def migrated_db(tmp_path): """Create a database with migrations applied.""" db_path = str(tmp_path / "test.db") migrations_dir = "migrations" run_migrations(db_path, migrations_dir, verbose=False) return get_db(db_path) class TestMigrationCreatesAllTables: """Tests that migration creates all reference tables.""" def test_species_table_exists(self, migrated_db): """Migration creates species table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='species'" ).fetchone() assert result is not None assert result[0] == "species" def test_locations_table_exists(self, migrated_db): """Migration creates locations table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='locations'" ).fetchone() assert result is not None assert result[0] == "locations" def test_products_table_exists(self, migrated_db): """Migration creates products table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='products'" ).fetchone() assert result is not None assert result[0] == "products" def test_feed_types_table_exists(self, migrated_db): """Migration creates feed_types table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='feed_types'" ).fetchone() assert result is not None assert result[0] == "feed_types" def test_users_table_exists(self, migrated_db): """Migration creates users table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='users'" ).fetchone() assert result is not None assert result[0] == "users" class TestSpeciesTable: """Tests for species table schema and constraints.""" def test_insert_valid_species(self, migrated_db): """Can insert valid species data.""" migrated_db.execute( """ INSERT INTO species (code, name, active, created_at_utc, updated_at_utc) VALUES ('duck', 'Duck', 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT code, name, active FROM species WHERE code='duck'" ).fetchone() assert result == ("duck", "Duck", 1) def test_active_check_constraint(self, migrated_db): """Species active must be 0 or 1.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO species (code, name, active, created_at_utc, updated_at_utc) VALUES ('invalid', 'Invalid', 2, 1704067200000, 1704067200000) """ ) def test_code_is_primary_key(self, migrated_db): """Species code is primary key (duplicate rejected).""" migrated_db.execute( """ INSERT INTO species (code, name, active, created_at_utc, updated_at_utc) VALUES ('goose', 'Goose', 1, 1704067200000, 1704067200000) """ ) with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO species (code, name, active, created_at_utc, updated_at_utc) VALUES ('goose', 'Another Goose', 1, 1704067200000, 1704067200000) """ ) class TestLocationsTable: """Tests for locations table schema and constraints.""" def test_insert_valid_location(self, migrated_db): """Can insert valid location data with 26-char ULID.""" migrated_db.execute( """ INSERT INTO locations (id, name, active, created_at_utc, updated_at_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAV', 'Strip 1', 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT id, name FROM locations WHERE name='Strip 1'" ).fetchone() assert result == ("01ARZ3NDEKTSV4RRFFQ69G5FAV", "Strip 1") def test_id_length_check_constraint(self, migrated_db): """Location ID must be exactly 26 characters.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO locations (id, name, active, created_at_utc, updated_at_utc) VALUES ('short', 'Bad Location', 1, 1704067200000, 1704067200000) """ ) def test_name_unique_constraint(self, migrated_db): """Location name must be unique.""" migrated_db.execute( """ INSERT INTO locations (id, name, active, created_at_utc, updated_at_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAV', 'Strip 1', 1, 1704067200000, 1704067200000) """ ) with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO locations (id, name, active, created_at_utc, updated_at_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAW', 'Strip 1', 1, 1704067200000, 1704067200000) """ ) class TestProductsTable: """Tests for products table schema and constraints.""" def test_insert_valid_product_piece(self, migrated_db): """Can insert valid product with piece unit.""" migrated_db.execute( """ INSERT INTO products (code, name, unit, collectable, sellable, active, created_at_utc, updated_at_utc) VALUES ('egg.duck', 'Duck Egg', 'piece', 1, 1, 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT code, unit FROM products WHERE code='egg.duck'" ).fetchone() assert result == ("egg.duck", "piece") def test_insert_valid_product_kg(self, migrated_db): """Can insert valid product with kg unit.""" migrated_db.execute( """ INSERT INTO products (code, name, unit, collectable, sellable, active, created_at_utc, updated_at_utc) VALUES ('meat', 'Meat', 'kg', 1, 1, 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute("SELECT unit FROM products WHERE code='meat'").fetchone() assert result[0] == "kg" def test_unit_check_constraint(self, migrated_db): """Product unit must be 'piece' or 'kg'.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO products (code, name, unit, collectable, sellable, active, created_at_utc, updated_at_utc) VALUES ('bad', 'Bad', 'liter', 1, 1, 1, 1704067200000, 1704067200000) """ ) def test_collectable_check_constraint(self, migrated_db): """Product collectable must be 0 or 1.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO products (code, name, unit, collectable, sellable, active, created_at_utc, updated_at_utc) VALUES ('bad', 'Bad', 'piece', 5, 1, 1, 1704067200000, 1704067200000) """ ) def test_sellable_check_constraint(self, migrated_db): """Product sellable must be 0 or 1.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO products (code, name, unit, collectable, sellable, active, created_at_utc, updated_at_utc) VALUES ('bad', 'Bad', 'piece', 1, -1, 1, 1704067200000, 1704067200000) """ ) class TestFeedTypesTable: """Tests for feed_types table schema and constraints.""" def test_insert_valid_feed_type(self, migrated_db): """Can insert valid feed type with protein percentage.""" migrated_db.execute( """ INSERT INTO feed_types (code, name, default_bag_size_kg, protein_pct, active, created_at_utc, updated_at_utc) VALUES ('layer', 'Layer Feed', 20, 16.5, 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT code, default_bag_size_kg, protein_pct FROM feed_types WHERE code='layer'" ).fetchone() assert result == ("layer", 20, 16.5) def test_insert_feed_type_without_protein(self, migrated_db): """Can insert feed type with NULL protein percentage.""" migrated_db.execute( """ INSERT INTO feed_types (code, name, default_bag_size_kg, protein_pct, active, created_at_utc, updated_at_utc) VALUES ('starter', 'Starter Feed', 25, NULL, 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT protein_pct FROM feed_types WHERE code='starter'" ).fetchone() assert result[0] is None def test_bag_size_check_constraint(self, migrated_db): """Feed type default_bag_size_kg must be >= 1.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO feed_types (code, name, default_bag_size_kg, protein_pct, active, created_at_utc, updated_at_utc) VALUES ('bad', 'Bad Feed', 0, NULL, 1, 1704067200000, 1704067200000) """ ) class TestUsersTable: """Tests for users table schema and constraints.""" def test_insert_valid_admin_user(self, migrated_db): """Can insert valid admin user.""" migrated_db.execute( """ INSERT INTO users (username, role, active, created_at_utc, updated_at_utc) VALUES ('ppetru', 'admin', 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT username, role FROM users WHERE username='ppetru'" ).fetchone() assert result == ("ppetru", "admin") def test_insert_valid_recorder_user(self, migrated_db): """Can insert valid recorder user.""" migrated_db.execute( """ INSERT INTO users (username, role, active, created_at_utc, updated_at_utc) VALUES ('ines', 'recorder', 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute("SELECT role FROM users WHERE username='ines'").fetchone() assert result[0] == "recorder" def test_role_check_constraint(self, migrated_db): """User role must be 'admin' or 'recorder'.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO users (username, role, active, created_at_utc, updated_at_utc) VALUES ('bad', 'superuser', 1, 1704067200000, 1704067200000) """ ) def test_username_is_primary_key(self, migrated_db): """Username is primary key (duplicate rejected).""" migrated_db.execute( """ INSERT INTO users (username, role, active, created_at_utc, updated_at_utc) VALUES ('guest', 'recorder', 1, 1704067200000, 1704067200000) """ ) with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO users (username, role, active, created_at_utc, updated_at_utc) VALUES ('guest', 'admin', 1, 1704067200000, 1704067200000) """ ) class TestDefaultValues: """Tests for default values in tables.""" def test_species_active_defaults_to_1(self, migrated_db): """Species active defaults to 1 when not specified.""" migrated_db.execute( """ INSERT INTO species (code, name, created_at_utc, updated_at_utc) VALUES ('sheep', 'Sheep', 1704067200000, 1704067200000) """ ) result = migrated_db.execute("SELECT active FROM species WHERE code='sheep'").fetchone() assert result[0] == 1 def test_locations_active_defaults_to_1(self, migrated_db): """Locations active defaults to 1 when not specified.""" migrated_db.execute( """ INSERT INTO locations (id, name, created_at_utc, updated_at_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAV', 'Nursery 1', 1704067200000, 1704067200000) """ ) result = migrated_db.execute( "SELECT active FROM locations WHERE name='Nursery 1'" ).fetchone() assert result[0] == 1 def test_products_active_defaults_to_1(self, migrated_db): """Products active defaults to 1 when not specified.""" migrated_db.execute( """ INSERT INTO products (code, name, unit, collectable, sellable, created_at_utc, updated_at_utc) VALUES ('offal', 'Offal', 'kg', 1, 1, 1704067200000, 1704067200000) """ ) result = migrated_db.execute("SELECT active FROM products WHERE code='offal'").fetchone() assert result[0] == 1 def test_feed_types_active_defaults_to_1(self, migrated_db): """Feed types active defaults to 1 when not specified.""" migrated_db.execute( """ INSERT INTO feed_types (code, name, default_bag_size_kg, created_at_utc, updated_at_utc) VALUES ('grower', 'Grower Feed', 20, 1704067200000, 1704067200000) """ ) result = migrated_db.execute("SELECT active FROM feed_types WHERE code='grower'").fetchone() assert result[0] == 1 def test_users_active_defaults_to_1(self, migrated_db): """Users active defaults to 1 when not specified.""" migrated_db.execute( """ INSERT INTO users (username, role, created_at_utc, updated_at_utc) VALUES ('newuser', 'recorder', 1704067200000, 1704067200000) """ ) result = migrated_db.execute("SELECT active FROM users WHERE username='newuser'").fetchone() assert result[0] == 1