Add time-series tracking tables for animal location, tag, and attribute history. Each interval represents a period when an animal had a specific state, with open intervals (end_utc=NULL) for current state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
6.0 KiB
Python
176 lines
6.0 KiB
Python
# ABOUTME: Tests for interval-related Pydantic models.
|
|
# ABOUTME: Validates LocationInterval, TagInterval, and AttrInterval models.
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from animaltrack.models.intervals import AttrInterval, LocationInterval, TagInterval
|
|
|
|
|
|
class TestLocationInterval:
|
|
"""Tests for the LocationInterval model."""
|
|
|
|
def test_valid_interval(self):
|
|
"""LocationInterval with all required fields validates successfully."""
|
|
interval = LocationInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
start_utc=1704067200000,
|
|
end_utc=1704153600000,
|
|
)
|
|
assert interval.animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAA"
|
|
assert interval.location_id == "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
assert interval.start_utc == 1704067200000
|
|
assert interval.end_utc == 1704153600000
|
|
|
|
def test_end_utc_defaults_to_none(self):
|
|
"""end_utc defaults to None (open interval)."""
|
|
interval = LocationInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.end_utc is None
|
|
|
|
def test_animal_id_must_be_26_chars(self):
|
|
"""LocationInterval with short animal_id raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
LocationInterval(
|
|
animal_id="short",
|
|
location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
start_utc=1704067200000,
|
|
)
|
|
|
|
def test_location_id_must_be_26_chars(self):
|
|
"""LocationInterval with short location_id raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
LocationInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
location_id="short",
|
|
start_utc=1704067200000,
|
|
)
|
|
|
|
|
|
class TestTagInterval:
|
|
"""Tests for the TagInterval model."""
|
|
|
|
def test_valid_interval(self):
|
|
"""TagInterval with all required fields validates successfully."""
|
|
interval = TagInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
tag="friendly",
|
|
start_utc=1704067200000,
|
|
end_utc=1704153600000,
|
|
)
|
|
assert interval.animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAA"
|
|
assert interval.tag == "friendly"
|
|
assert interval.start_utc == 1704067200000
|
|
assert interval.end_utc == 1704153600000
|
|
|
|
def test_end_utc_defaults_to_none(self):
|
|
"""end_utc defaults to None (open interval)."""
|
|
interval = TagInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
tag="leader",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.end_utc is None
|
|
|
|
def test_animal_id_must_be_26_chars(self):
|
|
"""TagInterval with short animal_id raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
TagInterval(
|
|
animal_id="short",
|
|
tag="friendly",
|
|
start_utc=1704067200000,
|
|
)
|
|
|
|
|
|
class TestAttrInterval:
|
|
"""Tests for the AttrInterval model."""
|
|
|
|
def test_valid_interval(self):
|
|
"""AttrInterval with all required fields validates successfully."""
|
|
interval = AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="sex",
|
|
value="female",
|
|
start_utc=1704067200000,
|
|
end_utc=1704153600000,
|
|
)
|
|
assert interval.animal_id == "01ARZ3NDEKTSV4RRFFQ69G5FAA"
|
|
assert interval.attr == "sex"
|
|
assert interval.value == "female"
|
|
assert interval.start_utc == 1704067200000
|
|
assert interval.end_utc == 1704153600000
|
|
|
|
def test_end_utc_defaults_to_none(self):
|
|
"""end_utc defaults to None (open interval)."""
|
|
interval = AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="status",
|
|
value="alive",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.end_utc is None
|
|
|
|
def test_animal_id_must_be_26_chars(self):
|
|
"""AttrInterval with short animal_id raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
AttrInterval(
|
|
animal_id="short",
|
|
attr="sex",
|
|
value="female",
|
|
start_utc=1704067200000,
|
|
)
|
|
|
|
def test_attr_sex_is_valid(self):
|
|
"""attr='sex' is a valid value."""
|
|
interval = AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="sex",
|
|
value="female",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.attr == "sex"
|
|
|
|
def test_attr_life_stage_is_valid(self):
|
|
"""attr='life_stage' is a valid value."""
|
|
interval = AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="life_stage",
|
|
value="adult",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.attr == "life_stage"
|
|
|
|
def test_attr_repro_status_is_valid(self):
|
|
"""attr='repro_status' is a valid value."""
|
|
interval = AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="repro_status",
|
|
value="intact",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.attr == "repro_status"
|
|
|
|
def test_attr_status_is_valid(self):
|
|
"""attr='status' is a valid value."""
|
|
interval = AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="status",
|
|
value="alive",
|
|
start_utc=1704067200000,
|
|
)
|
|
assert interval.attr == "status"
|
|
|
|
def test_attr_invalid_raises_error(self):
|
|
"""Invalid attr value raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
AttrInterval(
|
|
animal_id="01ARZ3NDEKTSV4RRFFQ69G5FAA",
|
|
attr="invalid_attr",
|
|
value="value",
|
|
start_utc=1704067200000,
|
|
)
|