innercontext/backend/tests/test_products.py
Piotr Oleszczyk 914c6087bd fix(products): work around Gemini int-enum schema rejection in parse-text
Gemini API rejects int-valued enums (StrengthLevel) in response_schema,
raising a validation error before any request is sent. Fix by introducing
AIActiveIngredient (inherits ActiveIngredient, overrides strength_level and
irritation_potential as Optional[int]) and ProductParseLLMResponse used only
as the Gemini schema. The two-step validation converts ints back to StrengthLevel
via Pydantic coercion. Adds a test covering the numeric strength level path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 22:00:48 +01:00

220 lines
6.5 KiB
Python

import uuid
def test_create_minimal(client, product_data):
r = client.post("/products", json=product_data)
assert r.status_code == 201
data = r.json()
assert "id" in data
assert data["name"] == product_data["name"]
assert data["brand"] == product_data["brand"]
assert data["category"] == "moisturizer"
def test_create_with_actives(client, product_data):
product_data["actives"] = [
{"name": "Niacinamide", "percent": 10.0, "functions": ["niacinamide"]}
]
r = client.post("/products", json=product_data)
assert r.status_code == 201
data = r.json()
assert len(data["actives"]) == 1
assert data["actives"][0]["name"] == "Niacinamide"
assert data["actives"][0]["percent"] == 10.0
def test_create_invalid_enum(client, product_data):
product_data["category"] = "not_a_category"
r = client.post("/products", json=product_data)
assert r.status_code == 422
def test_create_missing_required(client, product_data):
del product_data["name"]
r = client.post("/products", json=product_data)
assert r.status_code == 422
def test_list_empty(client):
r = client.get("/products")
assert r.status_code == 200
assert r.json() == []
def test_list_returns_created(client, created_product):
r = client.get("/products")
assert r.status_code == 200
ids = [p["id"] for p in r.json()]
assert created_product["id"] in ids
def test_list_filter_category(client, client_and_data=None):
# Create a moisturizer and a serum
base = {
"brand": "B",
"recommended_time": "both",
"leave_on": True,
}
r1 = client.post(
"/products", json={**base, "name": "Moist", "category": "moisturizer"}
)
r2 = client.post("/products", json={**base, "name": "Ser", "category": "serum"})
assert r1.status_code == 201
assert r2.status_code == 201
r = client.get("/products?category=moisturizer")
assert r.status_code == 200
data = r.json()
assert all(p["category"] == "moisturizer" for p in data)
names = [p["name"] for p in data]
assert "Moist" in names
assert "Ser" not in names
def test_list_filter_brand(client):
base = {
"recommended_time": "both",
"leave_on": True,
"category": "serum",
}
client.post("/products", json={**base, "name": "A1", "brand": "BrandA"})
client.post("/products", json={**base, "name": "B1", "brand": "BrandB"})
r = client.get("/products?brand=BrandA")
assert r.status_code == 200
data = r.json()
assert all(p["brand"] == "BrandA" for p in data)
assert len(data) == 1
def test_list_filter_is_medication(client):
base = {
"brand": "B",
"recommended_time": "both",
"leave_on": True,
"category": "serum",
}
client.post("/products", json={**base, "name": "Normal", "is_medication": False})
# is_medication=True requires usage_notes (model validator)
client.post(
"/products",
json={
**base,
"name": "Med",
"is_medication": True,
"usage_notes": "Apply pea-sized amount",
},
)
r = client.get("/products?is_medication=true")
assert r.status_code == 200
data = r.json()
assert all(p["is_medication"] is True for p in data)
assert len(data) == 1
assert data[0]["name"] == "Med"
def test_list_filter_targets(client):
base = {
"brand": "B",
"recommended_time": "both",
"leave_on": True,
"category": "serum",
}
client.post("/products", json={**base, "name": "Acne", "targets": ["acne"]})
client.post("/products", json={**base, "name": "Aging", "targets": ["aging"]})
r = client.get("/products?targets=acne")
assert r.status_code == 200
data = r.json()
assert len(data) == 1
assert data[0]["name"] == "Acne"
def test_get_by_id(client, created_product):
pid = created_product["id"]
r = client.get(f"/products/{pid}")
assert r.status_code == 200
assert r.json()["id"] == pid
def test_get_not_found(client):
r = client.get(f"/products/{uuid.uuid4()}")
assert r.status_code == 404
def test_update_name(client, created_product):
pid = created_product["id"]
r = client.patch(f"/products/{pid}", json={"name": "New Name"})
assert r.status_code == 200
assert r.json()["name"] == "New Name"
def test_update_json_field(client, created_product):
pid = created_product["id"]
r = client.patch(f"/products/{pid}", json={"inci": ["Water", "Glycerin"]})
assert r.status_code == 200
assert r.json()["inci"] == ["Water", "Glycerin"]
def test_update_not_found(client):
r = client.patch(f"/products/{uuid.uuid4()}", json={"name": "x"})
assert r.status_code == 404
def test_delete(client, created_product):
pid = created_product["id"]
r = client.delete(f"/products/{pid}")
assert r.status_code == 204
r2 = client.get(f"/products/{pid}")
assert r2.status_code == 404
def test_delete_not_found(client):
r = client.delete(f"/products/{uuid.uuid4()}")
assert r.status_code == 404
def test_list_inventory_empty(client, created_product):
pid = created_product["id"]
r = client.get(f"/products/{pid}/inventory")
assert r.status_code == 200
assert r.json() == []
def test_list_inventory_product_not_found(client):
r = client.get(f"/products/{uuid.uuid4()}/inventory")
assert r.status_code == 404
def test_create_inventory(client, created_product):
pid = created_product["id"]
r = client.post(f"/products/{pid}/inventory", json={"is_opened": False})
assert r.status_code == 201
data = r.json()
assert data["product_id"] == pid
assert data["is_opened"] is False
def test_create_inventory_product_not_found(client):
r = client.post(f"/products/{uuid.uuid4()}/inventory", json={})
assert r.status_code == 404
def test_parse_text_accepts_numeric_strength_levels(client, monkeypatch):
from innercontext.api import products as products_api
class _FakeResponse:
text = (
'{"name":"Test Serum","actives":[{"name":"Niacinamide","percent":10,'
'"functions":["niacinamide"],"strength_level":2,"irritation_potential":1}]}'
)
monkeypatch.setattr(products_api, "call_gemini", lambda **kwargs: _FakeResponse())
r = client.post("/products/parse-text", json={"text": "dummy input"})
assert r.status_code == 200
data = r.json()
assert data["name"] == "Test Serum"
assert data["actives"][0]["strength_level"] == 2
assert data["actives"][0]["irritation_potential"] == 1