FastAPI backend for personal health and skincare data with MCP export. Includes SQLModel models for products, inventory, medications, lab results, routines, and skin condition snapshots. Pytest suite with 111 tests running on SQLite in-memory (no PostgreSQL required). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
5.9 KiB
Python
198 lines
5.9 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",
|
|
"routine_role": "seal",
|
|
"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 = {
|
|
"routine_role": "seal",
|
|
"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",
|
|
"routine_role": "seal",
|
|
"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",
|
|
"routine_role": "seal",
|
|
"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
|