- upsert_slot: raise 422 immediately if meal_type not in VALID_MEAL_TYPES - update_prep_task: assert whitelist safety contract after dict comprehension - CreatePlanRequest: week_start typed as date with must_be_monday validator; str() cast at call site - PrepTask: frozen=True; build_prep_tasks rewired to use (priority, kwargs) tuples so frozen instances are built with correct sequence_order in one pass (no post-construction mutation) - Move deferred import json to file-level in meal_plans.py - Fix test dates: "2026-04-14" was a Tuesday; updated request bodies to "2026-04-13" (Monday)
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
# tests/api/test_meal_plans.py
|
|
"""Integration tests for /api/v1/meal-plans/ endpoints."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.cloud_session import get_session
|
|
from app.db.session import get_store
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
def _make_session(tier: str = "free") -> MagicMock:
|
|
m = MagicMock()
|
|
m.tier = tier
|
|
m.has_byok = False
|
|
return m
|
|
|
|
|
|
def _make_store() -> MagicMock:
|
|
m = MagicMock()
|
|
m.create_meal_plan.return_value = {
|
|
"id": 1, "week_start": "2026-04-14",
|
|
"meal_types": ["dinner"], "created_at": "2026-04-12T10:00:00",
|
|
}
|
|
m.list_meal_plans.return_value = []
|
|
m.get_meal_plan.return_value = None
|
|
m.get_plan_slots.return_value = []
|
|
m.upsert_slot.return_value = {
|
|
"id": 1, "plan_id": 1, "day_of_week": 0, "meal_type": "dinner",
|
|
"recipe_id": 42, "recipe_title": "Pasta", "servings": 2.0, "custom_label": None,
|
|
}
|
|
m.get_inventory.return_value = []
|
|
m.get_plan_recipes.return_value = []
|
|
m.get_prep_session_for_plan.return_value = None
|
|
m.create_prep_session.return_value = {
|
|
"id": 1, "plan_id": 1, "scheduled_date": "2026-04-13",
|
|
"status": "draft", "created_at": "2026-04-12T10:00:00",
|
|
}
|
|
m.get_prep_tasks.return_value = []
|
|
m.bulk_insert_prep_tasks.return_value = []
|
|
return m
|
|
|
|
|
|
@pytest.fixture()
|
|
def free_session():
|
|
session = _make_session("free")
|
|
store = _make_store()
|
|
app.dependency_overrides[get_session] = lambda: session
|
|
app.dependency_overrides[get_store] = lambda: store
|
|
yield store
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def paid_session():
|
|
session = _make_session("paid")
|
|
store = _make_store()
|
|
app.dependency_overrides[get_session] = lambda: session
|
|
app.dependency_overrides[get_store] = lambda: store
|
|
yield store
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_create_plan_free_tier_locks_to_dinner(free_session):
|
|
resp = client.post("/api/v1/meal-plans/", json={
|
|
"week_start": "2026-04-13", "meal_types": ["breakfast", "dinner"]
|
|
})
|
|
assert resp.status_code == 200
|
|
# Free tier forced to dinner-only regardless of request
|
|
free_session.create_meal_plan.assert_called_once_with("2026-04-13", ["dinner"])
|
|
|
|
|
|
def test_create_plan_paid_tier_respects_meal_types(paid_session):
|
|
resp = client.post("/api/v1/meal-plans/", json={
|
|
"week_start": "2026-04-13", "meal_types": ["breakfast", "lunch", "dinner"]
|
|
})
|
|
assert resp.status_code == 200
|
|
paid_session.create_meal_plan.assert_called_once_with(
|
|
"2026-04-13", ["breakfast", "lunch", "dinner"]
|
|
)
|
|
|
|
|
|
def test_list_plans_returns_200(free_session):
|
|
resp = client.get("/api/v1/meal-plans/")
|
|
assert resp.status_code == 200
|
|
assert resp.json() == []
|
|
|
|
|
|
def test_upsert_slot_returns_200(free_session):
|
|
free_session.get_meal_plan.return_value = {
|
|
"id": 1, "week_start": "2026-04-14", "meal_types": ["dinner"],
|
|
"created_at": "2026-04-12T10:00:00",
|
|
}
|
|
resp = client.put(
|
|
"/api/v1/meal-plans/1/slots/0/dinner",
|
|
json={"recipe_id": 42, "servings": 2.0},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_get_shopping_list_returns_200(free_session):
|
|
free_session.get_meal_plan.return_value = {
|
|
"id": 1, "week_start": "2026-04-14", "meal_types": ["dinner"],
|
|
"created_at": "2026-04-12T10:00:00",
|
|
}
|
|
resp = client.get("/api/v1/meal-plans/1/shopping-list")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "gap_items" in body
|
|
assert "covered_items" in body
|