# ABOUTME: Tests for reference table Pydantic models. # ABOUTME: Validates Species, Location, Product, FeedType, and User models. import pytest from pydantic import ValidationError from animaltrack.models.reference import ( FeedType, Location, Product, ProductUnit, Species, User, UserRole, ) class TestSpecies: """Tests for the Species model.""" def test_valid_species(self): """Species with all required fields validates successfully.""" species = Species( code="duck", name="Duck", active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert species.code == "duck" assert species.name == "Duck" assert species.active is True assert species.created_at_utc == 1704067200000 assert species.updated_at_utc == 1704067200000 def test_species_active_defaults_to_true(self): """Species active field defaults to True.""" species = Species( code="goose", name="Goose", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert species.active is True def test_species_requires_code(self): """Species without code raises ValidationError.""" with pytest.raises(ValidationError): Species( name="Duck", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) def test_species_requires_name(self): """Species without name raises ValidationError.""" with pytest.raises(ValidationError): Species( code="duck", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) def test_species_requires_timestamps(self): """Species without timestamps raises ValidationError.""" with pytest.raises(ValidationError): Species(code="duck", name="Duck") class TestLocation: """Tests for the Location model.""" def test_valid_location(self): """Location with valid 26-char ULID validates successfully.""" location = Location( id="01ARZ3NDEKTSV4RRFFQ69G5FAV", name="Strip 1", active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert location.id == "01ARZ3NDEKTSV4RRFFQ69G5FAV" assert location.name == "Strip 1" def test_location_id_must_be_26_chars(self): """Location with ID not exactly 26 chars raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: Location( id="short", name="Strip 1", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert "26 characters" in str(exc_info.value) def test_location_id_too_long(self): """Location with ID longer than 26 chars raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: Location( id="01ARZ3NDEKTSV4RRFFQ69G5FAVX", # 27 chars name="Strip 1", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert "26 characters" in str(exc_info.value) def test_location_active_defaults_to_true(self): """Location active field defaults to True.""" location = Location( id="01ARZ3NDEKTSV4RRFFQ69G5FAV", name="Strip 1", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert location.active is True def test_location_requires_name(self): """Location without name raises ValidationError.""" with pytest.raises(ValidationError): Location( id="01ARZ3NDEKTSV4RRFFQ69G5FAV", created_at_utc=1704067200000, updated_at_utc=1704067200000, ) class TestProductUnit: """Tests for the ProductUnit enum.""" def test_piece_value(self): """ProductUnit.PIECE has value 'piece'.""" assert ProductUnit.PIECE.value == "piece" def test_kg_value(self): """ProductUnit.KG has value 'kg'.""" assert ProductUnit.KG.value == "kg" class TestProduct: """Tests for the Product model.""" def test_valid_product(self): """Product with all required fields validates successfully.""" product = Product( code="egg.duck", name="Duck Egg", unit=ProductUnit.PIECE, collectable=True, sellable=True, active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert product.code == "egg.duck" assert product.name == "Duck Egg" assert product.unit == ProductUnit.PIECE assert product.collectable is True assert product.sellable is True def test_product_unit_kg(self): """Product with kg unit validates successfully.""" product = Product( code="meat", name="Meat", unit=ProductUnit.KG, collectable=True, sellable=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert product.unit == ProductUnit.KG def test_product_unit_from_string(self): """Product accepts unit as string value.""" product = Product( code="meat", name="Meat", unit="kg", collectable=True, sellable=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert product.unit == ProductUnit.KG def test_product_invalid_unit(self): """Product with invalid unit raises ValidationError.""" with pytest.raises(ValidationError): Product( code="meat", name="Meat", unit="invalid", collectable=True, sellable=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) def test_product_active_defaults_to_true(self): """Product active field defaults to True.""" product = Product( code="meat", name="Meat", unit=ProductUnit.KG, collectable=True, sellable=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert product.active is True def test_product_requires_collectable(self): """Product without collectable raises ValidationError.""" with pytest.raises(ValidationError): Product( code="meat", name="Meat", unit=ProductUnit.KG, sellable=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) class TestFeedType: """Tests for the FeedType model.""" def test_valid_feed_type(self): """FeedType with all required fields validates successfully.""" feed_type = FeedType( code="layer", name="Layer Feed", default_bag_size_kg=20, protein_pct=16.5, active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert feed_type.code == "layer" assert feed_type.name == "Layer Feed" assert feed_type.default_bag_size_kg == 20 assert feed_type.protein_pct == 16.5 def test_feed_type_protein_pct_optional(self): """FeedType protein_pct is optional (can be None).""" feed_type = FeedType( code="starter", name="Starter Feed", default_bag_size_kg=20, protein_pct=None, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert feed_type.protein_pct is None def test_feed_type_default_bag_size_must_be_positive(self): """FeedType default_bag_size_kg must be >= 1.""" with pytest.raises(ValidationError) as exc_info: FeedType( code="layer", name="Layer Feed", default_bag_size_kg=0, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert "greater than or equal to 1" in str(exc_info.value) def test_feed_type_default_bag_size_negative(self): """FeedType negative default_bag_size_kg raises ValidationError.""" with pytest.raises(ValidationError): FeedType( code="layer", name="Layer Feed", default_bag_size_kg=-5, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) def test_feed_type_active_defaults_to_true(self): """FeedType active field defaults to True.""" feed_type = FeedType( code="grower", name="Grower Feed", default_bag_size_kg=25, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert feed_type.active is True class TestUserRole: """Tests for the UserRole enum.""" def test_admin_value(self): """UserRole.ADMIN has value 'admin'.""" assert UserRole.ADMIN.value == "admin" def test_recorder_value(self): """UserRole.RECORDER has value 'recorder'.""" assert UserRole.RECORDER.value == "recorder" class TestUser: """Tests for the User model.""" def test_valid_user_admin(self): """User with admin role validates successfully.""" user = User( username="ppetru", role=UserRole.ADMIN, active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert user.username == "ppetru" assert user.role == UserRole.ADMIN def test_valid_user_recorder(self): """User with recorder role validates successfully.""" user = User( username="ines", role=UserRole.RECORDER, active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert user.role == UserRole.RECORDER def test_user_role_from_string(self): """User accepts role as string value.""" user = User( username="guest", role="recorder", active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert user.role == UserRole.RECORDER def test_user_invalid_role(self): """User with invalid role raises ValidationError.""" with pytest.raises(ValidationError): User( username="bad", role="superuser", active=True, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) def test_user_active_defaults_to_true(self): """User active field defaults to True.""" user = User( username="test", role=UserRole.RECORDER, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) assert user.active is True def test_user_requires_username(self): """User without username raises ValidationError.""" with pytest.raises(ValidationError): User( role=UserRole.ADMIN, created_at_utc=1704067200000, updated_at_utc=1704067200000, ) class TestTimestampValidation: """Tests for timestamp validation across all models.""" def test_species_negative_timestamp_rejected(self): """Species with negative timestamp raises ValidationError.""" with pytest.raises(ValidationError): Species( code="duck", name="Duck", created_at_utc=-1, updated_at_utc=1704067200000, ) def test_location_negative_timestamp_rejected(self): """Location with negative timestamp raises ValidationError.""" with pytest.raises(ValidationError): Location( id="01ARZ3NDEKTSV4RRFFQ69G5FAV", name="Strip 1", created_at_utc=1704067200000, updated_at_utc=-1, ) def test_product_zero_timestamp_allowed(self): """Product with zero timestamp (Unix epoch) is allowed.""" product = Product( code="egg", name="Egg", unit=ProductUnit.PIECE, collectable=True, sellable=True, created_at_utc=0, updated_at_utc=0, ) assert product.created_at_utc == 0