import unittest from fastapi.testclient import TestClient import main from db import connect, create class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.conn = await connect(":memory:") await create(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 a user and create two households r = self.client.post( "/api/v1/auth/register", json={"email": "r@test.com", "password": "pw", "displayName": "R"}, ) assert r.status_code == 200, r.text self.token = r.json()["accessToken"] self.headers = {"Authorization": f"Bearer {self.token}"} r = self.client.post("/api/v1/households", headers=self.headers, json={"name": "H1"}) assert r.status_code == 200, r.text self.h1 = r.json()["slug"] r = self.client.post("/api/v1/households", headers=self.headers, json={"name": "H2"}) assert r.status_code == 200, r.text self.h2 = r.json()["slug"] async def asyncTearDown(self): await self.conn.close() main.app.dependency_overrides.clear() def test_create_and_list_scoped_recipes(self): # Create a recipe in H1 recipe = { "id": -1, "name": "Soup", "link": "https://example.com/soup", "serves": 2, "imageUrls": [], "ingredients": [ { "id": 0, "line": "1 Apple", "name": "Apple", "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 rid = r.json()["id"] # createdBy fields present and match current user created_by = r.json().get("createdBy") assert created_by is not None assert isinstance(created_by.get("id"), int) assert created_by.get("displayName") == "R" assert r.json().get("createdById") == created_by["id"] # List H1 should include r = self.client.get(f"/api/v1/households/{self.h1}/recipes", headers=self.headers) assert r.status_code == 200 items = r.json()["items"] assert any(it["id"] == rid for it in items) # list items include createdBy found = next(it for it in items if it["id"] == rid) assert "createdBy" in found and "createdById" in found # List H2 should not include r = self.client.get(f"/api/v1/households/{self.h2}/recipes", headers=self.headers) assert r.status_code == 200 items2 = r.json()["items"] assert not any(it["id"] == rid for it in items2) # Get in H2 by id should 404 r = self.client.get(f"/api/v1/households/{self.h2}/recipes/{rid}", headers=self.headers) assert r.status_code == 404 def test_delete_recipe_scoped(self): # Create a recipe in H1 recipe = { "id": -1, "name": "ToDelete", "link": "https://example.com/del", "serves": 2, "imageUrls": [], "ingredients": [ { "id": 0, "line": "1 Apple", "name": "Apple", "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 rid = r.json()["id"] # createdBy present assert "createdBy" in r.json() and "createdById" in r.json() # Delete it via v2 scoped route r2 = self.client.delete(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers) assert r2.status_code == 200, r2.text # hiddenBy is populated on delete body_del = r2.json() assert "hiddenBy" in body_del and "hiddenById" in body_del assert body_del["hiddenBy"]["displayName"] == "R" # 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)