Ported v1 behavioral coverage to v2 counterparts across meals, recipes, shopping, and consumed flows.

This commit is contained in:
jableader 2025-11-01 16:35:39 +11:00
parent aa18a1502d
commit 50f908f204
5 changed files with 267 additions and 0 deletions

View file

@ -104,3 +104,23 @@ class TestMealsConsumedV2(unittest.IsolatedAsyncioTestCase):
assert r2.status_code == 200 assert r2.status_code == 200
cur2 = r2.json() cur2 = r2.json()
assert any(i.get("mealId") == self.meal_h2 for i in cur2["requestedMeals"]) # still present assert any(i.get("mealId") == self.meal_h2 for i in cur2["requestedMeals"]) # still present
async def test_mark_consumed_requires_timezone(self):
# Create another meal in H1
await self.conn.execute(
"INSERT INTO Meal (suggested_date, household_id) VALUES (datetime('now'), ?)",
(self.h1_id,),
)
async with self.conn.execute("SELECT last_insert_rowid()") as c:
row = await c.fetchone()
assert row is not None
meal_id = int(row[0])
# Missing tzinfo should 400
body = {"consumedDate": datetime.datetime.now().replace(tzinfo=None).isoformat()}
r = self.client.post(
f"/api/v1/households/{self.h1}/meals/{meal_id}/consumed",
headers=self.headers,
json=body,
)
assert r.status_code == 400
assert "timezone" in r.json()["title"].lower()

View file

@ -75,3 +75,91 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
f"/api/v1/households/{self.slug}/meals/{meal_id}", headers=self.headers f"/api/v1/households/{self.slug}/meals/{meal_id}", headers=self.headers
) )
assert r3.status_code == 200, r3.text assert r3.status_code == 200, r3.text
def test_create_meal_validation_errors(self):
# Base valid body
base = {
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
"chefs": [{"id": 1, "name": "A"}],
"cleanup": [{"id": 1, "name": "A"}],
"consumers": [{"id": 1, "name": "A"}],
"recipes": [],
"extraIngredients": [
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
],
}
# No chefs
body = dict(base)
body["chefs"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
assert r.status_code == 400
assert "chef" in r.json()["title"].lower()
# No cleanup
body = dict(base)
body["cleanup"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
assert r.status_code == 400
assert "cleanup" in r.json()["title"].lower()
# No consumers
body = dict(base)
body["consumers"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
assert r.status_code == 400
assert "consumer" in r.json()["title"].lower()
# No recipes or extra ingredients
body = dict(base)
body["recipes"] = []
body["extraIngredients"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
assert r.status_code == 400
assert "recipe" in r.json()["title"].lower() or "ingredient" in r.json()["title"].lower()
# Zero servings in recipe
body = dict(base)
body["recipes"] = [{"mealId": -1, "recipeId": 1, "servings": 0}]
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
assert r.status_code == 400
assert "servings" in r.json()["title"].lower()
def test_update_id_mismatch_and_not_found_and_delete_not_found(self):
# Create a valid meal first
body = {
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
"chefs": [{"id": 1, "name": "A"}],
"cleanup": [{"id": 1, "name": "A"}],
"consumers": [{"id": 1, "name": "A"}],
"recipes": [],
"extraIngredients": [
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
],
}
r = self.client.post(
f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body
)
assert r.status_code == 200, r.text
created = r.json()
# ID mismatch
mismatch = dict(created)
wrong_id = created["id"] + 123
r_mis = self.client.put(
f"/api/v1/households/{self.slug}/meals/{wrong_id}", headers=self.headers, json=mismatch
)
assert r_mis.status_code == 400
# Not found update
mismatch["id"] = wrong_id
r_nf = self.client.put(
f"/api/v1/households/{self.slug}/meals/{wrong_id}", headers=self.headers, json=mismatch
)
assert r_nf.status_code == 404
# Delete not found
r_del_nf = self.client.delete(
f"/api/v1/households/{self.slug}/meals/{wrong_id}", headers=self.headers
)
assert r_del_nf.status_code == 404

View file

@ -116,3 +116,97 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
# Subsequent get in same household should be 404 # Subsequent get in same household should be 404
r3 = self.client.get(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers) r3 = self.client.get(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers)
assert r3.status_code == 404 assert r3.status_code == 404
def test_create_recipe_requires_ingredients_and_cursor_edge(self):
# Create without ingredients -> 400
bad = {
"id": -1,
"name": "NoIngr",
"link": "https://example.com/no",
"serves": 2,
"imageUrls": [],
"ingredients": [],
}
r = self.client.post(
f"/api/v1/households/{self.h1}/recipes", headers=self.headers, json=bad
)
assert r.status_code == 400, r.text
# Seed two recipes then exercise cursor behavior
for i in range(2):
good = {
"id": -1,
"name": f"R{i}",
"link": f"https://example.com/r{i}",
"serves": 2,
"imageUrls": [],
"ingredients": [
{
"id": 0,
"line": "1 A",
"name": "A",
"unit": "Items",
"quantity": 1,
"preparation": "",
"product": None,
}
],
}
rr = self.client.post(
f"/api/v1/households/{self.h1}/recipes", headers=self.headers, json=good
)
assert rr.status_code == 200, rr.text
# invalid cursor -> treated as start
r1 = self.client.get(
f"/api/v1/households/{self.h1}/recipes?cursor=notanint&limit=1",
headers=self.headers,
)
assert r1.status_code == 200
body1 = r1.json()
assert "items" in body1
# After last
all_resp = self.client.get(
f"/api/v1/households/{self.h1}/recipes?limit=200", headers=self.headers
)
items = all_resp.json()["items"]
if items:
last_id = items[-1]["id"]
after = self.client.get(
f"/api/v1/households/{self.h1}/recipes?cursor={last_id}&limit=200",
headers=self.headers,
)
body_after = after.json()
assert body_after["items"] == [] or body_after.get("nextCursor") is None
def test_list_recipes_name_filter_q(self):
# Seed a uniquely named recipe
recipe = {
"id": -1,
"name": "UniqueNameZZZ",
"link": "https://example.com/unique",
"serves": 2,
"imageUrls": [],
"ingredients": [
{
"id": 0,
"line": "1 A",
"name": "A",
"unit": "Items",
"quantity": 1,
"preparation": "",
"product": None,
}
],
}
r = self.client.post(
f"/api/v1/households/{self.h1}/recipes", headers=self.headers, json=recipe
)
assert r.status_code == 200, r.text
# Name filter should find it
r2 = self.client.get(
f"/api/v1/households/{self.h1}/recipes?q=UniqueNameZZZ", headers=self.headers
)
assert r2.status_code == 200
items = r2.json()["items"]
assert any(it["name"] == "UniqueNameZZZ" for it in items)

View file

@ -0,0 +1,57 @@
import unittest
from fastapi.testclient import TestClient
import main
from db import connect, create
from scripts.migration_to_households import run_migration
class TestShoppingCurrentStructureV2(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.conn = await connect(":memory:")
await create(self.conn)
await run_migration(self.conn)
async def override_get_db():
try:
yield self.conn
finally:
pass
main.app.dependency_overrides[main.get_db] = override_get_db
self.client = TestClient(main.app)
# Register user and create a household
r = self.client.post(
"/api/v1/auth/register",
json={"email": "s2@test.com", "password": "pw", "displayName": "S2"},
)
assert r.status_code == 200, r.text
token = r.json()["accessToken"]
self.headers = {"Authorization": f"Bearer {token}"}
r = self.client.post("/api/v1/households", headers=self.headers, json={"name": "H"})
assert r.status_code == 200, r.text
self.slug = r.json()["slug"]
async def asyncTearDown(self):
await self.conn.close()
main.app.dependency_overrides.clear()
def test_current_payload_keys(self):
r = self.client.get(
f"/api/v1/households/{self.slug}/shopping/current",
headers=self.headers,
)
assert r.status_code == 200, r.text
body = r.json()
for key in (
"outstandingItems",
"requestedMeals",
"purchasedItems",
"shoppingListLookup",
"ingredientsLookup",
"recipesLookup",
"mealsLookup",
):
assert key in body

View file

@ -121,3 +121,11 @@ class TestShoppingPurchaseV2(unittest.IsolatedAsyncioTestCase):
) )
assert resp.status_code == 400, resp.text assert resp.status_code == 400, resp.text
assert "application/problem+json" in resp.headers.get("content-type", "") assert "application/problem+json" in resp.headers.get("content-type", "")
def test_purchase_unauthorized_without_bearer(self):
# Missing Authorization header should be 401 via get_current_user
resp = self.client.post(
f"/api/v1/households/{self.h1}/shopping",
json={"storeName": "woolworths", "items": []},
)
assert resp.status_code == 401