# ABOUTME: Tests for event log routes. # ABOUTME: Covers GET /event-log rendering and filtering by location. import os import time import pytest from starlette.testclient import TestClient from animaltrack.events.payloads import AnimalCohortCreatedPayload, ProductCollectedPayload from animaltrack.events.store import EventStore from animaltrack.projections import ( AnimalRegistryProjection, EventAnimalsProjection, EventLogProjection, IntervalProjection, ProductsProjection, ProjectionRegistry, ) from animaltrack.services.animal import AnimalService from animaltrack.services.products import ProductService def make_test_settings( csrf_secret: str = "test-secret", trusted_proxy_ips: str = "127.0.0.1", dev_mode: bool = True, ): """Create Settings for testing by setting env vars temporarily.""" from animaltrack.config import Settings old_env = os.environ.copy() try: os.environ["CSRF_SECRET"] = csrf_secret os.environ["TRUSTED_PROXY_IPS"] = trusted_proxy_ips os.environ["DEV_MODE"] = str(dev_mode).lower() return Settings() finally: os.environ.clear() os.environ.update(old_env) @pytest.fixture def client(seeded_db): """Create a test client for the app.""" from animaltrack.web.app import create_app settings = make_test_settings(trusted_proxy_ips="testclient") app, rt = create_app(settings=settings, db=seeded_db) return TestClient(app, raise_server_exceptions=True) @pytest.fixture def valid_location_id(seeded_db): """Get Strip 1 location ID from seeds.""" row = seeded_db.execute("SELECT id FROM locations WHERE name = 'Strip 1'").fetchone() return row[0] @pytest.fixture def strip2_location_id(seeded_db): """Get Strip 2 location ID from seeds.""" row = seeded_db.execute("SELECT id FROM locations WHERE name = 'Strip 2'").fetchone() return row[0] @pytest.fixture def animal_service(seeded_db): """Create an AnimalService for testing.""" event_store = EventStore(seeded_db) registry = ProjectionRegistry() registry.register(AnimalRegistryProjection(seeded_db)) registry.register(EventAnimalsProjection(seeded_db)) registry.register(IntervalProjection(seeded_db)) registry.register(EventLogProjection(seeded_db)) return AnimalService(seeded_db, event_store, registry) @pytest.fixture def product_service(seeded_db): """Create a ProductService for testing.""" event_store = EventStore(seeded_db) registry = ProjectionRegistry() registry.register(AnimalRegistryProjection(seeded_db)) registry.register(EventAnimalsProjection(seeded_db)) registry.register(IntervalProjection(seeded_db)) registry.register(ProductsProjection(seeded_db)) registry.register(EventLogProjection(seeded_db)) return ProductService(seeded_db, event_store, registry) def create_cohort(animal_service, location_id, count=3): """Helper to create a cohort and return animal IDs.""" ts_utc = int(time.time() * 1000) payload = AnimalCohortCreatedPayload( species="duck", count=count, life_stage="adult", sex="unknown", location_id=location_id, origin="purchased", ) event = animal_service.create_cohort(payload, ts_utc, "test_user") return event.entity_refs["animal_ids"] class TestEventLogRoute: """Tests for GET /event-log route.""" def test_event_log_without_location_shows_selector(self, client): """Event log without location_id shows location selector.""" response = client.get("/event-log") assert response.status_code == 200 # Should show location selector prompt assert "Select a location" in response.text def test_event_log_returns_empty_for_new_location(self, client, valid_location_id): """Event log returns empty state for location with no events.""" response = client.get(f"/event-log?location_id={valid_location_id}") assert response.status_code == 200 # Should show empty state message assert "No events" in response.text or "event-log" in response.text def test_event_log_shows_events(self, client, seeded_db, animal_service, valid_location_id): """Event log shows events for the location.""" # Create some animals (creates AnimalCohortCreated event) create_cohort(animal_service, valid_location_id, count=5) response = client.get(f"/event-log?location_id={valid_location_id}") assert response.status_code == 200 assert "AnimalCohortCreated" in response.text or "cohort" in response.text.lower() def test_event_log_shows_product_collected( self, client, seeded_db, animal_service, product_service, valid_location_id ): """Event log shows ProductCollected events.""" # Create animals first animal_ids = create_cohort(animal_service, valid_location_id, count=3) # Collect eggs ts_utc = int(time.time() * 1000) payload = ProductCollectedPayload( location_id=valid_location_id, product_code="egg.duck", quantity=5, resolved_ids=animal_ids, ) product_service.collect_product(payload, ts_utc, "test_user") response = client.get(f"/event-log?location_id={valid_location_id}") assert response.status_code == 200 assert "ProductCollected" in response.text or "egg" in response.text.lower() def test_event_log_filters_by_location( self, client, seeded_db, animal_service, valid_location_id, strip2_location_id ): """Event log only shows events for the specified location.""" # Create animals at location 1 create_cohort(animal_service, valid_location_id, count=3) # Create animals at location 2 create_cohort(animal_service, strip2_location_id, count=2) # Get events for location 2 only response = client.get(f"/event-log?location_id={strip2_location_id}") assert response.status_code == 200 # Should see location 2 events only # Count the events displayed text = response.text # Location 2 should have 1 event (cohort of 2) assert "count" in text.lower() or "2" in text def test_event_log_orders_by_time_descending( self, client, seeded_db, animal_service, product_service, valid_location_id ): """Event log shows newest events first.""" # Create cohort first animal_ids = create_cohort(animal_service, valid_location_id, count=3) # Then collect eggs ts_utc = int(time.time() * 1000) + 1000 payload = ProductCollectedPayload( location_id=valid_location_id, product_code="egg.duck", quantity=5, resolved_ids=animal_ids, ) product_service.collect_product(payload, ts_utc, "test_user") response = client.get(f"/event-log?location_id={valid_location_id}") assert response.status_code == 200 # ProductCollected should appear before AnimalCohortCreated (newer first) text = response.text # Check that the response contains both event types cohort_pos = text.find("Cohort") if "Cohort" in text else text.find("cohort") egg_pos = text.find("egg") if "egg" in text else text.find("Product") # Both should be present assert cohort_pos != -1 or egg_pos != -1 class TestEventLogPartial: """Tests for HTMX partial responses.""" def test_htmx_request_returns_partial( self, client, seeded_db, animal_service, valid_location_id ): """HTMX request returns partial HTML without full page wrapper.""" create_cohort(animal_service, valid_location_id) response = client.get( f"/event-log?location_id={valid_location_id}", headers={"HX-Request": "true"}, ) assert response.status_code == 200 # Partial should not have full page structure assert "