"""Tests for the Phase 5 weekly orchestration.""" import pytest from datetime import date, datetime, timezone from uuid import uuid4 pytestmark = pytest.mark.requires_postgres # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- WEEK = date(2026, 5, 8) # a Friday @pytest.fixture() def family(db): from app.models import FamilyProfile fp = FamilyProfile( id=uuid4(), name="Test Family", household_size=4, adult_count=2, child_count=2, pending_approval_policy="approve", ) db.add(fp) db.flush() return fp @pytest.fixture() def member(db, family): from app.models import FamilyMember, FamilyMemberRole m = FamilyMember( id=uuid4(), family_profile_id=family.id, name="Alice", email="alice@example.com", role=FamilyMemberRole.ADULT, ) db.add(m) db.flush() return m @pytest.fixture() def weekly_run(db, family): from app.models import WeeklyRun r = WeeklyRun(family_id=family.id, week_start_date=WEEK, status="pending") db.add(r) db.flush() return r @pytest.fixture() def weekly_run_scraped(db, family): from app.models import WeeklyRun r = WeeklyRun( family_id=family.id, week_start_date=WEEK, status="running", scraped_at=datetime.now(timezone.utc), ) db.add(r) db.flush() return r @pytest.fixture() def weekly_run_generated(db, family): from app.models import WeeklyRun r = WeeklyRun( family_id=family.id, week_start_date=WEEK, status="running", scraped_at=datetime.now(timezone.utc), generated_at=datetime.now(timezone.utc), ) db.add(r) db.flush() return r @pytest.fixture() def meal_plan(db, family): from app.models import MealPlan, MealPlanStatus mp = MealPlan( id=uuid4(), family_profile_id=family.id, week_start_date=WEEK, status=MealPlanStatus.PENDING_APPROVAL, ) db.add(mp) db.flush() return mp @pytest.fixture() def pending_item(db, meal_plan): from app.models import MealPlanItem, MealPlanItemStatus, MealType, Recipe recipe = Recipe( id=uuid4(), name="Test Pasta", servings=4, ingredients=[{"name": "pasta", "qty": "400", "unit": "g"}], instructions=["boil water", "cook pasta"], ) db.add(recipe) db.flush() item = MealPlanItem( id=uuid4(), meal_plan_id=meal_plan.id, recipe_id=recipe.id, day_of_week=5, meal_type=MealType.DINNER, approval_status=MealPlanItemStatus.PENDING, ) db.add(item) db.flush() return item @pytest.fixture() def weekly_run_emailed(db, family): from app.models import WeeklyRun r = WeeklyRun( family_id=family.id, week_start_date=WEEK, status="running", scraped_at=datetime.now(timezone.utc), generated_at=datetime.now(timezone.utc), emailed_at=datetime.now(timezone.utc), ) db.add(r) db.flush() return r # --------------------------------------------------------------------------- # alerts.py tests # --------------------------------------------------------------------------- def test_send_admin_alert_noop_when_email_unset(monkeypatch): monkeypatch.setattr( "app.services.orchestrator.alerts.settings", type("S", (), {"ADMIN_EMAIL": ""})(), ) from app.services.orchestrator.alerts import send_admin_alert send_admin_alert("test subject", "test body") # must not raise def test_send_admin_alert_calls_backend(monkeypatch): sent = [] class FakeBackend: def send(self, **kwargs): sent.append(kwargs) monkeypatch.setattr( "app.services.orchestrator.alerts.settings", type("S", (), {"ADMIN_EMAIL": "admin@example.com"})(), ) monkeypatch.setattr( "app.services.orchestrator.alerts.get_email_backend", lambda: FakeBackend(), ) from app.services.orchestrator.alerts import send_admin_alert send_admin_alert("boom", "details here") assert len(sent) == 1 assert "[MealPlanner ALERT] boom" in sent[0]["subject"] assert sent[0]["to"] == "admin@example.com" # --------------------------------------------------------------------------- # step_scrape tests # --------------------------------------------------------------------------- def test_step_scrape_idempotent(db, weekly_run_scraped): original_ts = weekly_run_scraped.scraped_at from app.services.orchestrator.steps import step_scrape step_scrape(weekly_run_scraped, db) assert weekly_run_scraped.scraped_at == original_ts def test_step_scrape_success(db, weekly_run, monkeypatch): monkeypatch.setattr( "app.services.orchestrator.steps.ScraperService.run_scrape", lambda self, **kw: {"status": "success", "items_scraped": 42}, ) from app.services.orchestrator.steps import step_scrape step_scrape(weekly_run, db) assert weekly_run.scraped_at is not None assert weekly_run.used_stale_data is False def test_step_scrape_both_fail_marks_stale(db, weekly_run, monkeypatch): monkeypatch.setattr( "app.services.orchestrator.steps.ScraperService.run_scrape", lambda self, **kw: {"status": "failed", "error": "timeout"}, ) monkeypatch.setattr( "app.services.orchestrator.steps.send_admin_alert", lambda subject, body: None, ) from app.services.orchestrator.steps import step_scrape step_scrape(weekly_run, db) assert weekly_run.used_stale_data is True assert weekly_run.scraped_at is not None