Ported v1 behavioral coverage to v2 counterparts across meals, recipes, shopping, and consumed flows.
This commit is contained in:
parent
aa18a1502d
commit
50f908f204
5 changed files with 267 additions and 0 deletions
|
|
@ -104,3 +104,23 @@ class TestMealsConsumedV2(unittest.IsolatedAsyncioTestCase):
|
|||
assert r2.status_code == 200
|
||||
cur2 = r2.json()
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -75,3 +75,91 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
|||
f"/api/v1/households/{self.slug}/meals/{meal_id}", headers=self.headers
|
||||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -116,3 +116,97 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
|||
# Subsequent get in same household should be 404
|
||||
r3 = self.client.get(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers)
|
||||
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)
|
||||
|
|
|
|||
57
tests/test_shopping_current_structure_v2.py
Normal file
57
tests/test_shopping_current_structure_v2.py
Normal 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
|
||||
|
|
@ -121,3 +121,11 @@ class TestShoppingPurchaseV2(unittest.IsolatedAsyncioTestCase):
|
|||
)
|
||||
assert resp.status_code == 400, resp.text
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue