import pytest import pytest_asyncio from fooder.controller.product import ProductController from fooder.model.product import ProductCreateModel @pytest_asyncio.fixture async def second_product(ctx): data = ProductCreateModel( name="Broccoli", protein=2.8, carb=7.0, fat=0.4, fiber=2.6 ) async with ctx.repo.transaction(): ctrl = await ProductController.create(ctx, data) return ctrl.obj async def test_list_products_orders_by_usage( auth_client, auth_ctx, product, second_product ): async with auth_ctx.repo.transaction(): await auth_ctx.repo.user_product_usage.increment( user_id=auth_ctx.user.id, product_id=second_product.id, count=5 ) response = await auth_client.get("/api/product") ids = [p["id"] for p in response.json()] assert ids.index(second_product.id) < ids.index(product.id) async def test_update_product_returns_200(auth_client, product): response = await auth_client.patch( f"/api/product/{product.id}", json={"name": "Updated Name"} ) assert response.status_code == 200 assert response.json()["name"] == "Updated Name" async def test_update_product_recalculates_calories_when_macros_change( auth_client, product ): response = await auth_client.patch( f"/api/product/{product.id}", json={"protein": 10.0} ) # protein=10, carb=0, fat=3.6, fiber=0 → 10*4 + 3.6*9 = 40 + 32.4 = 72.4 assert response.json()["calories"] == pytest.approx(72.4) async def test_update_product_uses_explicit_calories(auth_client, product): response = await auth_client.patch( f"/api/product/{product.id}", json={"protein": 10.0, "calories": 99.0} ) assert response.json()["calories"] == 99.0 async def test_update_product_not_found_returns_404(auth_client): response = await auth_client.patch("/api/product/99999", json={"name": "Ghost"}) assert response.status_code == 404 async def test_update_product_duplicate_barcode_returns_409( auth_client, product, product_payload ): await auth_client.post( "/api/product", json={**product_payload, "name": "Other", "barcode": "AAA"} ) response = await auth_client.patch( f"/api/product/{product.id}", json={"barcode": "AAA"} ) assert response.status_code == 409 async def test_update_product_without_auth_returns_401(client, product): response = await client.patch(f"/api/product/{product.id}", json={"name": "x"}) assert response.status_code == 401 async def test_create_product_returns_201(auth_client, product_payload): response = await auth_client.post("/api/product", json=product_payload) assert response.status_code == 201 async def test_create_product_returns_correct_fields(auth_client, product_payload): response = await auth_client.post("/api/product", json=product_payload) body = response.json() assert body["name"] == product_payload["name"] assert body["protein"] == product_payload["protein"] assert body["carb"] == product_payload["carb"] assert body["fat"] == product_payload["fat"] assert body["fiber"] == product_payload["fiber"] assert "id" in body async def test_create_product_calculates_calories(auth_client, product_payload): response = await auth_client.post("/api/product", json=product_payload) # 31*4 + 0*4 + 3.6*9 + 0*2 = 124 + 32.4 = 156.4 assert response.json()["calories"] == pytest.approx(156.4) async def test_create_product_uses_explicit_calories(auth_client, product_payload): response = await auth_client.post( "/api/product", json={**product_payload, "calories": 50.0} ) assert response.json()["calories"] == 50.0 async def test_create_product_duplicate_barcode_returns_409( auth_client, product_payload ): await auth_client.post( "/api/product", json={**product_payload, "barcode": "123456"} ) response = await auth_client.post( "/api/product", json={**product_payload, "barcode": "123456"} ) assert response.status_code == 409 async def test_create_product_invalid_protein_returns_422(auth_client, product_payload): response = await auth_client.post( "/api/product", json={**product_payload, "protein": -1.0} ) assert response.status_code == 422 async def test_create_product_protein_over_100_returns_422( auth_client, product_payload ): response = await auth_client.post( "/api/product", json={**product_payload, "protein": 101.0} ) assert response.status_code == 422 async def test_create_product_without_auth_returns_401(client, product_payload): response = await client.post("/api/product", json=product_payload) assert response.status_code == 401 async def test_list_products_returns_200(auth_client, product): response = await auth_client.get("/api/product") assert response.status_code == 200 assert isinstance(response.json(), list) async def test_list_products_contains_created(auth_client, product): response = await auth_client.get("/api/product") ids = [p["id"] for p in response.json()] assert product.id in ids async def test_list_products_filters_by_name(auth_client, product): response = await auth_client.get("/api/product", params={"q": product.name}) assert all(product.name.lower() in p["name"].lower() for p in response.json()) async def test_list_products_without_auth_returns_401(client): response = await client.get("/api/product") assert response.status_code == 401 async def test_get_by_barcode_returns_product_from_db( auth_client, product_with_barcode ): response = await auth_client.get( f"/api/product/barcode/{product_with_barcode.barcode}" ) assert response.status_code == 200 assert response.json()["id"] == product_with_barcode.id async def test_get_by_barcode_imports_when_not_in_db( auth_client, mock_product_finder, external_product ): response = await auth_client.get("/api/product/barcode/9999999999") assert response.status_code == 200 body = response.json() assert body["name"] == external_product.name assert body["protein"] == external_product.protein assert body["fat"] == external_product.fat assert body["carb"] == external_product.carb assert body["fiber"] == external_product.fiber assert body["calories"] == external_product.kcal assert body["barcode"] == "9999999999" async def test_get_by_barcode_persists_imported_product( auth_client, mock_product_finder ): await auth_client.get("/api/product/barcode/8888888888") response = await auth_client.get("/api/product/barcode/8888888888") assert response.status_code == 200 async def test_get_by_barcode_not_found_returns_404( auth_client, mock_product_finder_not_found ): response = await auth_client.get("/api/product/barcode/0000000000") assert response.status_code == 404 async def test_get_by_barcode_without_auth_returns_401(client): response = await client.get("/api/product/barcode/1234567890") assert response.status_code == 401