import pytest
import httpx
import json
import tandoor_api
from pytest_httpx import HTTPXMock
from tandoor_api import (
recipes,
meal_plan,
shopping_list,
lookup,
format_date,
extract_results,
)
class TestHelpers:
def test_format_date_adds_time(self):
assert format_date("2024-01-15") == "2024-01-15T00:00:00"
def test_format_date_preserves_time(self):
assert format_date("2024-01-15T12:30:00") == "2024-01-15T12:30:00"
def test_extract_results_from_paginated(self):
data = {"count": 2, "next": None, "previous": None, "results": [{"id": 1}, {"id": 2}]}
assert extract_results(data) == [{"id": 1}, {"id": 2}]
def test_extract_results_from_list(self):
data = [{"id": 1}, {"id": 2}]
assert extract_results(data) == [{"id": 1}, {"id": 2}]
def test_extract_results_from_empty_dict(self):
data = {}
assert extract_results(data) == []
class TestRecipes:
def test_recipes_list(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?limit=10",
json={
"count": 1,
"results": [
{
"id": 1,
"name": "Test Recipe",
"rating": 5,
"keywords": [{"label": "dinner"}],
}
],
},
)
result = recipes(operation="list")
assert result["total"] == 1
assert result["recipes"][0]["name"] == "Test Recipe"
assert result["recipes"][0]["keywords"] == ["dinner"]
def test_recipes_list_with_query(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?limit=10&query=pasta",
json={"count": 1, "results": [{"id": 2, "name": "Pasta", "rating": 4, "keywords": []}]},
)
result = recipes(operation="list", query="pasta")
assert result["total"] == 1
assert result["recipes"][0]["name"] == "Pasta"
def test_recipes_get_requires_id(self):
result = recipes(operation="get")
assert "error" in result
def test_recipes_get_not_found(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Missing",
json={"count": 0, "results": []},
)
result = recipes(operation="get", recipe_id="Missing")
assert "error" in result
assert "not found" in result["error"].lower()
def test_recipes_get(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Ramen",
json={"count": 1, "results": [{"id": 7, "name": "Ramen"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/7/",
json={
"id": 7,
"name": "Ramen",
"servings": 2,
"servings_text": "2 servings",
"working_time": 10,
"waiting_time": 5,
"rating": 4,
"keywords": [{"label": "noodles"}],
"steps": [
{
"order": 0,
"name": "Prep",
"instruction": "Boil water",
"time": 5,
"ingredients": [
{"amount": 2, "unit": {"name": "cups"}, "food": {"name": "water"}},
],
}
],
},
)
result = recipes(operation="get", recipe_id="Ramen")
assert result["id"] == 7
assert result["name"] == "Ramen"
assert result["ingredients"][0]["food"] == "water"
def test_recipes_create_requires_name(self):
result = recipes(operation="create")
assert "error" in result
def test_recipes_create_with_ingredients(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/",
method="POST",
json={"id": 11, "name": "Toast"},
)
httpx_mock.add_response(
url="http://test-tandoor/api/step/",
method="POST",
json={"id": 21, "recipe": 11},
)
httpx_mock.add_response(
url="http://test-tandoor/api/ingredient/",
method="POST",
json={"id": 31},
)
httpx_mock.add_response(
url="http://test-tandoor/api/ingredient/",
method="POST",
json={"id": 32},
)
result = recipes(
operation="create",
name="Toast",
ingredients="1 slice bread\n2 tbsp butter",
instructions="Toast it",
)
assert result["success"] is True
assert result["id"] == 11
def test_recipes_create_ingredient_parse_fallback(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/",
method="POST",
json={"id": 12, "name": "Soup"},
)
httpx_mock.add_response(
url="http://test-tandoor/api/step/",
method="POST",
json={"id": 22, "recipe": 12},
)
httpx_mock.add_response(
url="http://test-tandoor/api/ingredient/",
method="POST",
json={"id": 41},
)
result = recipes(
operation="create",
name="Soup",
ingredients="two cups broth",
instructions="Stir",
)
assert result["success"] is True
ingredient_request = next(
req for req in httpx_mock.get_requests() if req.url.path == "/api/ingredient/"
)
payload = json.loads(ingredient_request.content.decode())
assert payload["amount"] is None
assert payload["food"]["name"] == "two cups broth"
def test_recipes_import_requires_url(self):
result = recipes(operation="import")
assert "error" in result
def test_recipes_import_url_not_found(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/missing",
method="HEAD",
status_code=404,
)
result = recipes(operation="import", url="https://example.com/missing")
assert result["success"] is False
assert "not found" in result["error"].lower()
def test_recipes_import_url_head_405_fallback_to_get(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/head-405",
method="HEAD",
status_code=405,
)
httpx_mock.add_response(
url="https://example.com/head-405",
method="GET",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={
"recipe_id": 66,
"recipe": {"name": "Fallback OK"},
},
)
result = recipes(operation="import", url="https://example.com/head-405")
assert result["success"] is True
assert result["id"] == 66
def test_recipes_import_url_timeout(self, monkeypatch):
def _timeout(*_args, **_kwargs):
raise httpx.TimeoutException("timeout")
monkeypatch.setattr(tandoor_api.httpx, "head", _timeout)
result = recipes(operation="import", url="https://example.com/slow")
assert result["success"] is False
assert "timed out" in result["error"].lower()
def test_recipes_import_invalid_name(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/blank",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={"recipe": {"name": "", "steps": []}},
)
result = recipes(operation="import", url="https://example.com/blank")
assert result["success"] is False
assert "no recipe name" in result["error"].lower()
def test_recipes_import_insufficient_content(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/empty",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={"recipe": {"name": "Empty", "steps": [{"instruction": "", "ingredients": []}]}},
)
result = recipes(operation="import", url="https://example.com/empty")
assert result["success"] is False
assert "insufficient recipe content" in result["error"].lower()
def test_recipes_import_duplicate(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/ramen",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={
"recipe_id": 44,
"recipe": {"name": "Imported Ramen"},
},
)
result = recipes(operation="import", url="https://example.com/ramen")
assert result["success"] is True
assert result["duplicate"] is True
assert result["id"] == 44
def test_recipes_import_defaults_servings(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/blank-name",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={
"recipe": {
"name": "Blank Name",
"steps": [{"instruction": "x" * 120, "ingredients": []}],
"image_url": None,
}
},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/",
method="POST",
json={"id": 88, "name": "Blank Name"},
)
result = recipes(operation="import", url="https://example.com/blank-name")
assert result["success"] is True
recipe_request = next(
req for req in httpx_mock.get_requests() if req.url.path == "/api/recipe/"
)
payload = json.loads(recipe_request.content.decode())
assert payload["name"] == "Blank Name"
assert payload["servings"] == 4
assert payload["servings_text"] == "4 servings"
def test_recipes_import_image_download_error(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/image-fail",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={
"recipe": {
"name": "Image Fail",
"steps": [{"instruction": "x" * 120, "ingredients": []}],
"image_url": "https://images.example.com/ramen.jpg",
"servings": 2,
}
},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/",
method="POST",
json={"id": 99, "name": "Image Fail"},
)
httpx_mock.add_response(
url="https://images.example.com/ramen.jpg",
method="GET",
status_code=500,
)
result = recipes(operation="import", url="https://example.com/image-fail")
assert result["success"] is True
assert result["image_uploaded"] is False
assert "image_error" in result
def test_recipes_import_image_selects_largest(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/images",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={
"recipe": {
"name": "Image Picks",
"steps": [{"instruction": "x" * 120, "ingredients": []}],
},
"images": [
"https://img.example.com/photo-160x160.jpg",
"https://img.example.com/photo-800x600.jpg",
],
},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/",
method="POST",
json={"id": 123, "name": "Image Picks"},
)
httpx_mock.add_response(
url="https://img.example.com/photo.jpg",
method="GET",
status_code=200,
headers={"content-type": "image/jpeg"},
content=b"img",
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/123/image/",
method="PUT",
status_code=200,
json={},
)
result = recipes(operation="import", url="https://example.com/images")
assert result["success"] is True
assert result["image_uploaded"] is True
def test_recipes_import_batch_mixed(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/ok",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
json={
"recipe_id": 55,
"recipe": {"name": "Batch OK"},
},
)
httpx_mock.add_response(
url="https://example.com/bad",
method="HEAD",
status_code=404,
)
result = recipes(operation="import", urls=["https://example.com/ok", "https://example.com/bad"])
assert result["total"] == 2
assert result["success"] == 1
assert result["errors"] == 1
def test_recipes_list_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?limit=10",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
recipes(operation="list")
def test_recipes_get_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Ramen",
json={"count": 1, "results": [{"id": 7, "name": "Ramen"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/7/",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
recipes(operation="get", recipe_id="Ramen")
def test_recipes_create_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/",
method="POST",
status_code=400,
json={"detail": "bad request"},
)
with pytest.raises(httpx.HTTPStatusError):
recipes(operation="create", name="Bad")
def test_recipes_import_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://example.com/source",
method="HEAD",
status_code=200,
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe-from-source/",
method="POST",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
recipes(operation="import", url="https://example.com/source")
# --- New tests for history, log, suggest ---
def test_recipes_history(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/cook-log/",
json={
"count": 2,
"results": [
{"id": 1, "recipe": 5, "recipe_name": "Pasta", "created_at": "2024-01-15T18:00:00Z", "servings": 2, "rating": 4, "comment": "Good"},
{"id": 2, "recipe": 7, "recipe_name": "Soup", "created_at": "2024-01-10T12:00:00Z", "servings": 4, "rating": 5, "comment": None},
],
},
)
result = recipes(operation="history")
assert result["total"] == 2
assert result["cook_logs"][0]["recipe_name"] == "Pasta"
assert result["cook_logs"][0]["rating"] == 4
def test_recipes_history_with_date_filter(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/cook-log/",
json={
"count": 3,
"results": [
{"id": 1, "recipe": 5, "created_at": "2024-01-20T18:00:00Z", "servings": 2, "rating": 4, "comment": None},
{"id": 2, "recipe": 7, "created_at": "2024-01-15T12:00:00Z", "servings": 4, "rating": 5, "comment": None},
{"id": 3, "recipe": 9, "created_at": "2024-01-05T12:00:00Z", "servings": 1, "rating": 3, "comment": None},
],
},
)
result = recipes(operation="history", from_date="2024-01-10", to_date="2024-01-18")
assert result["total"] == 1
assert result["cook_logs"][0]["id"] == 2
def test_recipes_history_with_recipe_filter(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/cook-log/?recipe=5",
json={
"count": 1,
"results": [
{"id": 1, "recipe": 5, "created_at": "2024-01-15T18:00:00Z", "servings": 2, "rating": 4, "comment": None},
],
},
)
result = recipes(operation="history", recipe_id="5")
assert result["total"] == 1
def test_recipes_log_requires_recipe_id(self):
result = recipes(operation="log")
assert "error" in result
def test_recipes_log(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Pasta",
json={"count": 1, "results": [{"id": 5, "name": "Pasta"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/cook-log/",
method="POST",
json={"id": 10, "recipe": 5, "created_at": "2024-01-20T19:00:00Z"},
)
result = recipes(operation="log", recipe_id="Pasta", servings=2, log_rating=5, comment="Delicious")
assert result["success"] is True
assert result["id"] == 10
def test_recipes_log_invalid_rating(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Pasta",
json={"count": 1, "results": [{"id": 5, "name": "Pasta"}]},
)
result = recipes(operation="log", recipe_id="Pasta", log_rating=10)
assert "error" in result
assert "rating" in result["error"].lower()
def test_recipes_suggest_no_on_hand(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?food_onhand=true&limit=500",
json={"count": 0, "results": []},
)
result = recipes(operation="suggest")
assert result["suggestions"] == []
assert "no foods" in result["message"].lower()
def test_recipes_suggest(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?food_onhand=true&limit=500",
json={"count": 2, "results": [{"id": 1, "name": "pasta"}, {"id": 2, "name": "garlic"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?limit=100",
json={"count": 1, "results": [{"id": 10, "name": "Garlic Pasta"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/10/",
json={
"id": 10,
"name": "Garlic Pasta",
"steps": [
{"ingredients": [{"food": {"id": 1, "name": "pasta"}}, {"food": {"id": 2, "name": "garlic"}}, {"food": {"id": 3, "name": "olive oil"}}]}
],
},
)
result = recipes(operation="suggest", min_match=0.5)
assert result["total"] == 1
assert result["suggestions"][0]["recipe_name"] == "Garlic Pasta"
assert result["suggestions"][0]["match_percent"] == 0.67
assert "olive oil" in result["suggestions"][0]["missing"]
class TestMealPlan:
def test_meal_plan_types(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-type/",
json={
"count": 3,
"results": [
{"id": 1, "name": "Breakfast", "order": 0},
{"id": 2, "name": "Lunch", "order": 1},
{"id": 3, "name": "Dinner", "order": 2},
],
},
)
result = meal_plan(operation="types")
assert result["total"] == 3
assert result["meal_types"][0]["name"] == "Breakfast"
def test_meal_plan_list(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/",
json={"count": 0, "results": []},
)
result = meal_plan(operation="list")
assert result["total"] == 0
def test_meal_plan_list_strips_time(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/?from_date=2024-01-01&to_date=2024-01-02",
json={"count": 0, "results": []},
)
result = meal_plan(
operation="list",
from_date="2024-01-01T00:00:00",
to_date="2024-01-02T05:00:00",
)
assert result["total"] == 0
def test_meal_plan_create_requires_fields(self):
result = meal_plan(operation="create")
assert "error" in result
def test_meal_plan_create_recipe_not_found(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Missing",
json={"count": 0, "results": []},
)
result = meal_plan(operation="create", recipe="Missing", date="2024-01-10", meal_type=2)
assert "error" in result
assert "not found" in result["error"].lower()
def test_meal_plan_create(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Soup",
json={"count": 1, "results": [{"id": 9, "name": "Soup"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/",
method="POST",
json={"id": 101},
)
result = meal_plan(operation="create", recipe="Soup", date="2024-01-10", meal_type=2)
assert result["success"] is True
assert result["id"] == 101
def test_meal_plan_list_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/",
status_code=502,
json={"detail": "bad gateway"},
)
with pytest.raises(httpx.HTTPStatusError):
meal_plan(operation="list")
def test_meal_plan_types_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-type/",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
meal_plan(operation="types")
def test_meal_plan_create_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/recipe/?query=Soup",
json={"count": 1, "results": [{"id": 9, "name": "Soup"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/",
method="POST",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
meal_plan(operation="create", recipe="Soup", date="2024-01-10", meal_type=2)
def test_meal_plan_create_type_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-type/",
method="POST",
status_code=400,
json={"detail": "bad request"},
)
with pytest.raises(httpx.HTTPStatusError):
meal_plan(operation="create_type", type_name="Brunch", type_order=3)
# --- New tests for delete ---
def test_meal_plan_delete_requires_id(self):
result = meal_plan(operation="delete")
assert "error" in result
def test_meal_plan_delete(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/55/",
method="DELETE",
status_code=204,
)
result = meal_plan(operation="delete", meal_plan_id=55)
assert result["success"] is True
assert "55" in result["message"]
def test_meal_plan_delete_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/meal-plan/99/",
method="DELETE",
status_code=404,
json={"detail": "not found"},
)
with pytest.raises(httpx.HTTPStatusError):
meal_plan(operation="delete", meal_plan_id=99)
class TestShoppingList:
def test_shopping_list_list(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/",
json={
"count": 1,
"results": [
{
"id": 1,
"amount": 2,
"unit": {"name": "cups"},
"food": {"name": "flour"},
"checked": False,
"recipe_mealplan": None,
}
],
},
)
result = shopping_list(operation="list")
assert result["total"] == 1
assert result["items"][0]["food"] == "flour"
def test_shopping_list_list_checked_filter(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/?checked=false",
json={"count": 0, "results": []},
)
result = shopping_list(operation="list", checked=False)
assert result["total"] == 0
def test_shopping_list_add_uses_name_for_unknown_food_unit(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?query=oat%20milk",
json={"count": 0, "results": []},
)
httpx_mock.add_response(
url="http://test-tandoor/api/unit/?query=carton",
json={"count": 0, "results": []},
)
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/",
method="POST",
json={"id": 77},
)
result = shopping_list(operation="add", food="oat milk", amount=1, unit="carton")
assert result["success"] is True
add_request = next(
req for req in httpx_mock.get_requests() if req.url.path == "/api/shopping-list-entry/"
)
payload = json.loads(add_request.content.decode())
assert payload["food"]["name"] == "oat milk"
assert payload["unit"]["name"] == "carton"
def test_shopping_list_add_requires_food(self):
result = shopping_list(operation="add")
assert "error" in result
def test_shopping_list_update_requires_item_id(self):
result = shopping_list(operation="update")
assert "error" in result
def test_shopping_list_remove_requires_item_id(self):
result = shopping_list(operation="remove")
assert "error" in result
def test_shopping_list_update_requires_fields(self):
result = shopping_list(operation="update", item_id=3)
assert "error" in result
def test_shopping_list_update(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/5/",
method="PATCH",
status_code=200,
json={},
)
result = shopping_list(operation="update", item_id=5, checked=True)
assert result["success"] is True
assert result["id"] == 5
def test_shopping_list_remove(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/6/",
method="DELETE",
status_code=204,
json={},
)
result = shopping_list(operation="remove", item_id=6)
assert result["success"] is True
def test_shopping_list_list_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/",
status_code=503,
json={"detail": "down"},
)
with pytest.raises(httpx.HTTPStatusError):
shopping_list(operation="list")
def test_shopping_list_add_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?query=milk",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
shopping_list(operation="add", food="milk")
def test_shopping_list_update_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/5/",
method="PATCH",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
shopping_list(operation="update", item_id=5, checked=True)
def test_shopping_list_remove_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/6/",
method="DELETE",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
shopping_list(operation="remove", item_id=6)
# --- New tests for check and clear ---
def test_shopping_list_check_requires_items(self):
result = shopping_list(operation="check")
assert "error" in result
def test_shopping_list_check_by_ids(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/1/",
method="PATCH",
status_code=200,
json={},
)
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/2/",
method="PATCH",
status_code=200,
json={},
)
result = shopping_list(operation="check", item_ids=[1, 2], checked=True)
assert result["success"] is True
assert result["checked_count"] == 2
def test_shopping_list_check_by_names(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/",
json={
"count": 2,
"results": [
{"id": 10, "food": {"name": "Milk"}},
{"id": 11, "food": {"name": "Eggs"}},
],
},
)
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/10/",
method="PATCH",
status_code=200,
json={},
)
result = shopping_list(operation="check", items=["Milk"], checked=True)
assert result["success"] is True
assert result["checked_count"] == 1
assert result["items"][0]["food"] == "Milk"
def test_shopping_list_check_not_found(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/",
json={"count": 0, "results": []},
)
result = shopping_list(operation="check", items=["Missing Item"])
assert result["success"] is False
assert result["errors"][0]["food"] == "Missing Item"
def test_shopping_list_clear_no_items(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/?checked=true",
json={"count": 0, "results": []},
)
result = shopping_list(operation="clear")
assert result["success"] is True
assert result["cleared_count"] == 0
def test_shopping_list_clear_with_pantry_update(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/?checked=true",
json={
"count": 2,
"results": [
{"id": 1, "food": {"id": 100, "name": "Milk"}},
{"id": 2, "food": {"id": 101, "name": "Eggs"}},
],
},
)
httpx_mock.add_response(url="http://test-tandoor/api/food/100/", method="PATCH", status_code=200, json={})
httpx_mock.add_response(url="http://test-tandoor/api/food/101/", method="PATCH", status_code=200, json={})
httpx_mock.add_response(url="http://test-tandoor/api/shopping-list-entry/1/", method="DELETE", status_code=204)
httpx_mock.add_response(url="http://test-tandoor/api/shopping-list-entry/2/", method="DELETE", status_code=204)
result = shopping_list(operation="clear", update_pantry=True)
assert result["success"] is True
assert result["cleared_count"] == 2
assert result["pantry_updated"] is True
assert "Milk" in result["foods_marked_on_hand"]
assert "Eggs" in result["foods_marked_on_hand"]
def test_shopping_list_clear_without_pantry_update(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/shopping-list-entry/?checked=true",
json={
"count": 1,
"results": [{"id": 1, "food": {"id": 100, "name": "Milk"}}],
},
)
httpx_mock.add_response(url="http://test-tandoor/api/shopping-list-entry/1/", method="DELETE", status_code=204)
result = shopping_list(operation="clear", update_pantry=False)
assert result["success"] is True
assert result["cleared_count"] == 1
assert "foods_marked_on_hand" not in result
class TestLookup:
def test_lookup_keywords(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/keyword/?limit=20",
json={"count": 1, "results": [{"id": 1, "label": "vegetarian"}]},
)
result = lookup(type="keywords")
assert result["total"] == 1
assert result["keywords"][0]["name"] == "vegetarian"
def test_lookup_foods(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?limit=20&query=chicken",
json={"count": 1, "results": [{"id": 5, "name": "chicken breast"}]},
)
result = lookup(type="foods", query="chicken")
assert result["total"] == 1
assert result["foods"][0]["name"] == "chicken breast"
def test_lookup_units(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/unit/?limit=20",
json={
"count": 2,
"results": [
{"id": 1, "name": "cups"},
{"id": 2, "name": "tbsp"},
],
},
)
result = lookup(type="units")
assert result["total"] == 2
def test_lookup_invalid_type(self):
result = lookup(type="invalid")
assert "error" in result
def test_lookup_keywords_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/keyword/?limit=20",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
lookup(type="keywords")
def test_lookup_foods_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?limit=20",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
lookup(type="foods")
def test_lookup_units_http_error_propagates(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/unit/?limit=20",
status_code=500,
json={"detail": "boom"},
)
with pytest.raises(httpx.HTTPStatusError):
lookup(type="units")
# --- New tests for pantry ---
def test_lookup_pantry_list(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?food_onhand=true&limit=20",
json={
"count": 2,
"results": [
{"id": 1, "name": "Milk"},
{"id": 2, "name": "Eggs"},
],
},
)
result = lookup(type="pantry")
assert result["total"] == 2
assert result["pantry_items"][0]["food_name"] == "Milk"
assert result["pantry_items"][0]["on_hand"] is True
def test_lookup_pantry_list_with_query(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?food_onhand=true&limit=20&query=milk",
json={"count": 1, "results": [{"id": 1, "name": "Milk"}]},
)
result = lookup(type="pantry", query="milk")
assert result["total"] == 1
def test_lookup_pantry_update_requires_on_hand(self):
result = lookup(type="pantry", action="update", food_id=1)
assert "error" in result
def test_lookup_pantry_update_requires_food(self):
result = lookup(type="pantry", action="update", on_hand=True)
assert "error" in result
def test_lookup_pantry_update_by_id(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/5/",
method="GET",
json={"id": 5, "name": "Chicken"},
)
httpx_mock.add_response(
url="http://test-tandoor/api/food/5/",
method="PATCH",
status_code=200,
json={},
)
result = lookup(type="pantry", action="update", food_id=5, on_hand=True)
assert result["success"] is True
assert result["food_name"] == "Chicken"
assert result["on_hand"] is True
def test_lookup_pantry_update_by_name(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?query=Butter",
json={"count": 1, "results": [{"id": 10, "name": "Butter"}]},
)
httpx_mock.add_response(
url="http://test-tandoor/api/food/10/",
method="PATCH",
status_code=200,
json={},
)
result = lookup(type="pantry", action="update", food_name="Butter", on_hand=False)
assert result["success"] is True
assert result["food_name"] == "Butter"
assert result["on_hand"] is False
assert "not on hand" in result["message"]
def test_lookup_pantry_update_food_not_found(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="http://test-tandoor/api/food/?query=Missing",
json={"count": 0, "results": []},
)
result = lookup(type="pantry", action="update", food_name="Missing", on_hand=True)
assert "error" in result
assert "not found" in result["error"].lower()
def test_lookup_pantry_invalid_action(self):
result = lookup(type="pantry", action="invalid")
assert "error" in result