Task 13: StyleAdapter with 5 cuisine templates (Italian, Latin, East Asian,
Eastern European, Mediterranean). Each template includes weighted method_bias
(sums to 1.0), element-filtered aromatics/depth/structure helpers, and
seasoning/finishing-fat vectors. StyleTemplate is a fully immutable frozen
dataclass with tuple fields.
Task 14: LLMRecipeGenerator for Levels 3 and 4. Level 3 builds a structured
element-scaffold prompt; Level 4 generates a minimal wildcard prompt (<1500
chars). Allergy hard-exclusion wired through RecipeRequest.allergies into
both prompt builders and the generate() call path. Parsed LLM response
(title, ingredients, directions, notes) fully propagated to RecipeSuggestion.
Task 15: User settings key-value store. Migration 012 adds user_settings
table. Store.get_setting / set_setting with upsert. GET/PUT /settings/{key}
endpoints with Pydantic SettingBody, key allowlist, get_session dependency.
RecipeEngine reads cooking_equipment from settings when hard_day_mode=True.
55 tests passing.
110 lines
3.4 KiB
Python
110 lines
3.4 KiB
Python
"""Tests for user settings 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
|
|
from app.models.schemas.recipe import RecipeRequest
|
|
from app.services.recipe.recipe_engine import RecipeEngine
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
def _make_session(tier: str = "free", has_byok: bool = False) -> MagicMock:
|
|
mock = MagicMock()
|
|
mock.tier = tier
|
|
mock.has_byok = has_byok
|
|
return mock
|
|
|
|
|
|
def _make_store() -> MagicMock:
|
|
mock = MagicMock()
|
|
mock.get_setting.return_value = None
|
|
mock.set_setting.return_value = None
|
|
mock.search_recipes_by_ingredients.return_value = []
|
|
mock.check_and_increment_rate_limit.return_value = (True, 1)
|
|
return mock
|
|
|
|
|
|
@pytest.fixture()
|
|
def tmp_store() -> MagicMock:
|
|
session_mock = _make_session()
|
|
store_mock = _make_store()
|
|
app.dependency_overrides[get_session] = lambda: session_mock
|
|
app.dependency_overrides[get_store] = lambda: store_mock
|
|
yield store_mock
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def test_set_and_get_cooking_equipment(tmp_store: MagicMock) -> None:
|
|
"""PUT then GET round-trips the cooking_equipment value."""
|
|
equipment_json = '["oven", "stovetop"]'
|
|
|
|
# PUT stores the value
|
|
put_resp = client.put(
|
|
"/api/v1/settings/cooking_equipment",
|
|
json={"value": equipment_json},
|
|
)
|
|
assert put_resp.status_code == 200
|
|
assert put_resp.json()["key"] == "cooking_equipment"
|
|
assert put_resp.json()["value"] == equipment_json
|
|
tmp_store.set_setting.assert_called_once_with("cooking_equipment", equipment_json)
|
|
|
|
# GET returns the stored value
|
|
tmp_store.get_setting.return_value = equipment_json
|
|
get_resp = client.get("/api/v1/settings/cooking_equipment")
|
|
assert get_resp.status_code == 200
|
|
assert get_resp.json()["value"] == equipment_json
|
|
|
|
|
|
def test_get_missing_setting_returns_404(tmp_store: MagicMock) -> None:
|
|
"""GET an allowed key that was never set returns 404."""
|
|
tmp_store.get_setting.return_value = None
|
|
resp = client.get("/api/v1/settings/cooking_equipment")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_hard_day_mode_uses_equipment_setting(tmp_store: MagicMock) -> None:
|
|
"""RecipeEngine.suggest() respects cooking_equipment from store when hard_day_mode=True."""
|
|
equipment_json = '["microwave"]'
|
|
tmp_store.get_setting.return_value = equipment_json
|
|
|
|
engine = RecipeEngine(store=tmp_store)
|
|
req = RecipeRequest(
|
|
pantry_items=["rice", "water"],
|
|
level=1,
|
|
constraints=[],
|
|
hard_day_mode=True,
|
|
)
|
|
|
|
result = engine.suggest(req)
|
|
|
|
# Engine should have read the equipment setting
|
|
tmp_store.get_setting.assert_called_with("cooking_equipment")
|
|
# Result is a valid RecipeResult (no crash)
|
|
assert result is not None
|
|
assert hasattr(result, "suggestions")
|
|
|
|
|
|
def test_put_unknown_key_returns_422(tmp_store: MagicMock) -> None:
|
|
"""PUT to an unknown settings key returns 422."""
|
|
resp = client.put(
|
|
"/api/v1/settings/nonexistent_key",
|
|
json={"value": "something"},
|
|
)
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_put_null_value_returns_422(tmp_store: MagicMock) -> None:
|
|
"""PUT with a null value returns 422 (Pydantic validation)."""
|
|
resp = client.put(
|
|
"/api/v1/settings/cooking_equipment",
|
|
json={"value": None},
|
|
)
|
|
assert resp.status_code == 422
|