Public Access
tests: fix suite-wide collection and failures
- config: switch Settings to ConfigDict(extra='ignore') so extra env vars (spoonacular_api_key, SWIFTLY_BEARER_TOKEN) don't crash import. Remove deprecated class Config. - email: wrap SendGrid imports in try/except so the module loads without the optional dependency. Update test_email_backend to patch Mail/RepyTo. - planner_select: default PlannerConfig.set_size=21 (3 meals/day × 7) is way too large for the unit test assertion that checks 3-recipe diversity. Introduced _CFG_3 with set_size=3 and applied to all tests. - Delete stale test_matcher.py importing removed functions. Full suite: 46 passed, 74 skipped (Postgres), 0 failed, 120 collected.
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import model_validator
|
||||
from pydantic import ConfigDict
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
# Required — fail fast at import time if unset.
|
||||
DATABASE_URL: str = ""
|
||||
|
||||
@@ -38,8 +41,7 @@ class Settings(BaseSettings):
|
||||
FAMILY_EMAIL_2: Optional[str] = None
|
||||
RECIPES_EMAIL: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
model_config = ConfigDict(extra="ignore", env_file=".env")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_database_url(self) -> "Settings":
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""
|
||||
Email backends.
|
||||
"""Email backends.
|
||||
|
||||
R2-B spike: only ConsoleEmailBackend is functional. SendGrid is a stub
|
||||
intentionally left to be wired in R3-C.
|
||||
@@ -13,8 +12,13 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional, Protocol
|
||||
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail, ReplyTo
|
||||
try:
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail, ReplyTo
|
||||
except ImportError: # pragma: no cover — SendGrid installed separately.
|
||||
SendGridAPIClient = None # type: ignore[misc]
|
||||
Mail = None # type: ignore[misc]
|
||||
ReplyTo = None # type: ignore[misc]
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
@@ -2,37 +2,59 @@
|
||||
import pytest
|
||||
|
||||
|
||||
class _FakeMail:
|
||||
def __init__(self, **kw):
|
||||
self._kw = kw
|
||||
self.reply_to = None
|
||||
|
||||
|
||||
class _FakeReplyTo:
|
||||
def __init__(self, addr):
|
||||
self.addr = addr
|
||||
|
||||
|
||||
class _FakeResponseOK:
|
||||
status_code = 202
|
||||
body = b""
|
||||
|
||||
|
||||
class _FakeResponse400:
|
||||
status_code = 400
|
||||
body = b"bad request"
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self, resp):
|
||||
self._resp = resp
|
||||
|
||||
def send(self, msg):
|
||||
self.last_msg = msg
|
||||
return self._resp
|
||||
|
||||
|
||||
def _patch_sendgrid(monkeypatch, resp):
|
||||
fake_client = _FakeClient(resp)
|
||||
monkeypatch.setattr(
|
||||
"app.services.email.SendGridAPIClient",
|
||||
lambda _key: fake_client,
|
||||
)
|
||||
monkeypatch.setattr("app.services.email.Mail", _FakeMail)
|
||||
monkeypatch.setattr("app.services.email.ReplyTo", _FakeReplyTo)
|
||||
return fake_client
|
||||
|
||||
|
||||
def test_sendgrid_send_calls_api(monkeypatch):
|
||||
calls = []
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 202
|
||||
body = b""
|
||||
|
||||
class FakeClient:
|
||||
def send(self, msg):
|
||||
calls.append(msg)
|
||||
return FakeResponse()
|
||||
|
||||
monkeypatch.setattr("app.services.email.SendGridAPIClient", lambda _key: FakeClient())
|
||||
client = _patch_sendgrid(monkeypatch, _FakeResponseOK())
|
||||
|
||||
from app.services.email import SendGridEmailBackend
|
||||
backend = SendGridEmailBackend()
|
||||
backend.send(to="to@example.com", subject="Hello", html="<p>Hi</p>")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert hasattr(client, "last_msg")
|
||||
|
||||
|
||||
def test_sendgrid_raises_on_4xx(monkeypatch):
|
||||
class FakeResponse:
|
||||
status_code = 400
|
||||
body = b"bad request"
|
||||
|
||||
class FakeClient:
|
||||
def send(self, msg):
|
||||
return FakeResponse()
|
||||
|
||||
monkeypatch.setattr("app.services.email.SendGridAPIClient", lambda _key: FakeClient())
|
||||
_patch_sendgrid(monkeypatch, _FakeResponse400())
|
||||
|
||||
from app.services.email import SendGridEmailBackend
|
||||
backend = SendGridEmailBackend()
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from app.services.matcher import build_match_pool, rank_candidates
|
||||
|
||||
|
||||
def test_build_match_pool_includes_aliases() -> None:
|
||||
ingredients = [
|
||||
{"id": "i1", "name": "Chicken Thighs", "aliases": ["chicken thigh"]},
|
||||
{"id": "i2", "name": "Chicken Breast", "aliases": []},
|
||||
]
|
||||
pool = build_match_pool(ingredients)
|
||||
# 1 name + 1 alias for i1, 1 name for i2 = 3 entries
|
||||
assert len(pool) == 3
|
||||
assert ("Chicken Thighs", "i1") in pool
|
||||
assert ("chicken thigh", "i1") in pool
|
||||
|
||||
|
||||
def test_rank_candidates_top_n_with_threshold() -> None:
|
||||
pool = [
|
||||
("Chicken Thighs", "i1"),
|
||||
("chicken thigh", "i1"),
|
||||
("Chicken Breast", "i2"),
|
||||
("Pork Chops", "i3"),
|
||||
]
|
||||
ranked = rank_candidates(
|
||||
target="Foster Farms Chicken Thighs Family Pack",
|
||||
pool=pool,
|
||||
top_n=3,
|
||||
threshold=0.75,
|
||||
)
|
||||
ids = [item["ingredient_id"] for item in ranked]
|
||||
assert ids[0] == "i1"
|
||||
assert all(item["confidence"] >= 0.75 for item in ranked)
|
||||
|
||||
|
||||
def test_rank_candidates_drops_below_threshold() -> None:
|
||||
pool = [("Pork Chops", "i3")]
|
||||
ranked = rank_candidates(
|
||||
target="Frosted Flakes Cereal 18oz",
|
||||
pool=pool,
|
||||
top_n=3,
|
||||
threshold=0.75,
|
||||
)
|
||||
assert ranked == []
|
||||
@@ -7,6 +7,10 @@ from app.services.planner.types import RecipeCost, ScoredRecipe
|
||||
|
||||
|
||||
_CFG = PlannerConfig()
|
||||
_CFG_3 = PlannerConfig(w_savings=0.30, w_coverage=0.25, w_pantry=0.10, w_time=0.15, w_recency=0.20,
|
||||
set_size=3, top_k=20, p_protein=0.15, p_cuisine=0.10, recency_weeks=4,
|
||||
calorie_tolerance_pct=20, max_total_minutes=45, max_meal_cost=30.00,
|
||||
time_ideal_minutes=25, time_full_minutes=45, recency_full_weeks=12)
|
||||
|
||||
|
||||
def _mk(score, protein, cuisine):
|
||||
@@ -34,7 +38,7 @@ def test_diversity_penalty_zero_when_all_unique():
|
||||
a = _mk(0.5, "chicken", "american")
|
||||
b = _mk(0.5, "beef", "mexican")
|
||||
c = _mk(0.5, "fish", "italian")
|
||||
assert set_diversity_penalty([a, b, c], _CFG) == 0.0
|
||||
assert set_diversity_penalty([a, b, c], _CFG_3) == 0.0
|
||||
|
||||
|
||||
def test_diversity_penalty_three_chickens():
|
||||
@@ -42,7 +46,7 @@ def test_diversity_penalty_three_chickens():
|
||||
b = _mk(0.5, "chicken", "italian")
|
||||
c = _mk(0.5, "chicken", "mexican")
|
||||
# 3 protein pairs * 0.15 = 0.45, no cuisine pairs
|
||||
p = set_diversity_penalty([a, b, c], _CFG)
|
||||
p = set_diversity_penalty([a, b, c], _CFG_3)
|
||||
assert abs(p - 0.45) < 1e-6
|
||||
|
||||
|
||||
@@ -60,7 +64,7 @@ def test_select_set_picks_diverse_over_homogeneous():
|
||||
mix2 = _mk(0.90, "chicken", "mexican") # shares protein with high
|
||||
mix3 = _mk(0.90, "fish", "american") # shares cuisine with high
|
||||
|
||||
chosen, set_score = select_set([high1, high2, high3, mix1, mix2, mix3], _CFG)
|
||||
chosen, set_score = select_set([high1, high2, high3, mix1, mix2, mix3], _CFG_3)
|
||||
chosen_ids = {s.recipe_id for s in chosen}
|
||||
assert chosen_ids == {mix1.recipe_id, mix2.recipe_id, mix3.recipe_id}
|
||||
|
||||
@@ -68,11 +72,11 @@ def test_select_set_picks_diverse_over_homogeneous():
|
||||
def test_select_set_handles_too_few():
|
||||
a = _mk(0.5, "x", "y")
|
||||
b = _mk(0.5, "x", "y")
|
||||
chosen, _ = select_set([a, b], _CFG)
|
||||
chosen, _ = select_set([a, b], _CFG_3)
|
||||
assert len(chosen) == 2 # less than set_size returns what we have
|
||||
|
||||
|
||||
def test_select_set_empty_input():
|
||||
chosen, score = select_set([], _CFG)
|
||||
chosen, score = select_set([], _CFG_3)
|
||||
assert chosen == []
|
||||
assert score == 0.0
|
||||
|
||||
Reference in New Issue
Block a user