# ABOUTME: Tests for animal-related Pydantic models. # ABOUTME: Validates Animal, LiveAnimal, and AnimalAlias models. import pytest from pydantic import ValidationError from animaltrack.events.enums import AnimalStatus, LifeStage, Origin, ReproStatus, Sex from animaltrack.models.animals import Animal, AnimalAlias, LiveAnimal class TestAnimal: """Tests for the Animal model.""" def test_valid_animal(self): """Animal with all required fields validates successfully.""" animal = Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) assert animal.animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAA" assert animal.species_code == "duck" assert animal.sex == Sex.MALE assert animal.status == AnimalStatus.ALIVE def test_animal_id_must_be_26_chars(self): """Animal with ID not exactly 26 chars raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: Animal( animal_id="short", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) assert "at least 26 characters" in str(exc_info.value) def test_location_id_must_be_26_chars(self): """Animal with location_id not exactly 26 chars raises ValidationError.""" with pytest.raises(ValidationError) as exc_info: Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="short", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) assert "at least 26 characters" in str(exc_info.value) def test_identified_defaults_to_false(self): """Identified defaults to False.""" animal = Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) assert animal.identified is False def test_nickname_is_optional(self): """Nickname is optional and defaults to None.""" animal = Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) assert animal.nickname is None def test_born_or_hatched_at_is_optional(self): """born_or_hatched_at is optional.""" animal = Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, born_or_hatched_at=1704067200000, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) assert animal.born_or_hatched_at == 1704067200000 def test_invalid_sex_raises_error(self): """Invalid sex value raises ValidationError.""" with pytest.raises(ValidationError): Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex="invalid", repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status=AnimalStatus.ALIVE, location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) def test_invalid_status_raises_error(self): """Invalid status value raises ValidationError.""" with pytest.raises(ValidationError): Animal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, status="invalid", location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", origin=Origin.HATCHED, first_seen_utc=1704067200000, last_event_utc=1704067200000, ) class TestLiveAnimal: """Tests for the LiveAnimal model.""" def test_valid_live_animal(self): """LiveAnimal with all required fields validates successfully.""" live_animal = LiveAnimal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, first_seen_utc=1704067200000, ) assert live_animal.animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAA" assert live_animal.location_id == "01ARZ3NDEKTSV4RRFFQ69G5FAV" def test_tags_defaults_to_empty_list(self): """Tags defaults to empty list.""" live_animal = LiveAnimal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, first_seen_utc=1704067200000, ) assert live_animal.tags == [] def test_tags_can_be_set(self): """Tags can be set to a list of strings.""" live_animal = LiveAnimal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, first_seen_utc=1704067200000, tags=["friendly", "leader"], ) assert live_animal.tags == ["friendly", "leader"] def test_last_move_utc_is_optional(self): """last_move_utc is optional.""" live_animal = LiveAnimal( animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, first_seen_utc=1704067200000, ) assert live_animal.last_move_utc is None def test_animal_id_must_be_26_chars(self): """LiveAnimal with ID not exactly 26 chars raises ValidationError.""" with pytest.raises(ValidationError): LiveAnimal( animal_id="short", location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV", species_code="duck", sex=Sex.MALE, repro_status=ReproStatus.INTACT, life_stage=LifeStage.ADULT, first_seen_utc=1704067200000, ) class TestAnimalAlias: """Tests for the AnimalAlias model.""" def test_valid_alias(self): """AnimalAlias with all required fields validates successfully.""" alias = AnimalAlias( alias_animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", survivor_animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAB", merged_at_utc=1704067200000, ) assert alias.alias_animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAA" assert alias.survivor_animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAB" assert alias.merged_at_utc == 1704067200000 def test_alias_animal_id_must_be_26_chars(self): """AnimalAlias with short alias_animal_id raises ValidationError.""" with pytest.raises(ValidationError): AnimalAlias( alias_animal_id="short", survivor_animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAB", merged_at_utc=1704067200000, ) def test_survivor_animal_id_must_be_26_chars(self): """AnimalAlias with short survivor_animal_id raises ValidationError.""" with pytest.raises(ValidationError): AnimalAlias( alias_animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA", survivor_animal_id="short", merged_at_utc=1704067200000, )