fooder-api/fooder/test/view/test_product.py

203 lines
6.9 KiB
Python

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