Initial commit: backend API, data models, and test suite
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>
This commit is contained in:
commit
8f7d893a63
32 changed files with 6282 additions and 0 deletions
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
87
backend/tests/conftest.py
Normal file
87
backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import os
|
||||
|
||||
# Must be set before importing db (which calls create_engine at module level)
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite://")
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
from sqlmodel.pool import StaticPool
|
||||
|
||||
import db as db_module
|
||||
from db import get_session
|
||||
from main import app
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def session(monkeypatch):
|
||||
"""Per-test fresh SQLite in-memory database with full isolation."""
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
# Patch before TestClient triggers lifespan (which calls create_db_and_tables)
|
||||
monkeypatch.setattr(db_module, "engine", engine)
|
||||
import innercontext.models # noqa: F401 — populate SQLModel.metadata
|
||||
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as s:
|
||||
yield s
|
||||
# monkeypatch auto-restores db_module.engine after test
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(session):
|
||||
"""TestClient using the per-test session for every request."""
|
||||
|
||||
def _override():
|
||||
yield session
|
||||
|
||||
app.dependency_overrides[get_session] = _override
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# ---- Shared data fixtures ----
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def product_data():
|
||||
return {
|
||||
"name": "CeraVe Moisturising Cream",
|
||||
"brand": "CeraVe",
|
||||
"category": "moisturizer",
|
||||
"routine_role": "seal",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_product(client, product_data):
|
||||
r = client.post("/products/", json=product_data)
|
||||
assert r.status_code == 201
|
||||
return r.json()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def medication_data():
|
||||
return {"kind": "prescription", "product_name": "Epiduo"}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_medication(client, medication_data):
|
||||
r = client.post("/health/medications", json=medication_data)
|
||||
assert r.status_code == 201
|
||||
return r.json()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_routine(client):
|
||||
r = client.post(
|
||||
"/routines/", json={"routine_date": "2026-02-26", "part_of_day": "am"}
|
||||
)
|
||||
assert r.status_code == 201
|
||||
return r.json()
|
||||
285
backend/tests/test_health.py
Normal file
285
backend/tests/test_health.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import uuid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Medications
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_medication_minimal(client, medication_data):
|
||||
r = client.post("/health/medications", json=medication_data)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert "record_id" in data
|
||||
assert data["kind"] == "prescription"
|
||||
assert data["product_name"] == "Epiduo"
|
||||
|
||||
|
||||
def test_create_medication_invalid_kind(client):
|
||||
r = client.post(
|
||||
"/health/medications", json={"kind": "invalid_kind", "product_name": "X"}
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_list_medications_empty(client):
|
||||
r = client.get("/health/medications")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
|
||||
def test_list_filter_kind(client):
|
||||
client.post(
|
||||
"/health/medications", json={"kind": "prescription", "product_name": "A"}
|
||||
)
|
||||
client.post(
|
||||
"/health/medications", json={"kind": "supplement", "product_name": "B"}
|
||||
)
|
||||
r = client.get("/health/medications?kind=supplement")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["kind"] == "supplement"
|
||||
|
||||
|
||||
def test_list_filter_product_name(client):
|
||||
client.post(
|
||||
"/health/medications", json={"kind": "otc", "product_name": "Epiduo Forte"}
|
||||
)
|
||||
client.post(
|
||||
"/health/medications", json={"kind": "otc", "product_name": "Panoxyl"}
|
||||
)
|
||||
r = client.get("/health/medications?product_name=epid")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert "Epiduo" in data[0]["product_name"]
|
||||
|
||||
|
||||
def test_get_medication(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.get(f"/health/medications/{mid}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["record_id"] == mid
|
||||
|
||||
|
||||
def test_get_medication_not_found(client):
|
||||
r = client.get(f"/health/medications/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_update_medication(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.patch(
|
||||
f"/health/medications/{mid}", json={"notes": "Take in the evening"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["notes"] == "Take in the evening"
|
||||
|
||||
|
||||
def test_update_medication_not_found(client):
|
||||
r = client.patch(f"/health/medications/{uuid.uuid4()}", json={"notes": "x"})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete_medication_no_usages(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.delete(f"/health/medications/{mid}")
|
||||
assert r.status_code == 204
|
||||
r2 = client.get(f"/health/medications/{mid}")
|
||||
assert r2.status_code == 404
|
||||
|
||||
|
||||
def test_delete_medication_with_usages(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
# Create a usage
|
||||
client.post(
|
||||
f"/health/medications/{mid}/usages",
|
||||
json={"valid_from": "2026-01-01T00:00:00"},
|
||||
)
|
||||
# Delete medication (should also delete usages)
|
||||
r = client.delete(f"/health/medications/{mid}")
|
||||
assert r.status_code == 204
|
||||
r2 = client.get(f"/health/medications/{mid}")
|
||||
assert r2.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Usages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_usage(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.post(
|
||||
f"/health/medications/{mid}/usages",
|
||||
json={"valid_from": "2026-01-01T08:00:00", "dose_value": 1.0, "dose_unit": "pea"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert "record_id" in data
|
||||
assert data["medication_record_id"] == mid
|
||||
assert data["dose_unit"] == "pea"
|
||||
|
||||
|
||||
def test_create_usage_medication_not_found(client):
|
||||
r = client.post(
|
||||
f"/health/medications/{uuid.uuid4()}/usages",
|
||||
json={"valid_from": "2026-01-01T00:00:00"},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_list_usages_empty(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.get(f"/health/medications/{mid}/usages")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
|
||||
def test_list_usages_returns_entries(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
client.post(
|
||||
f"/health/medications/{mid}/usages",
|
||||
json={"valid_from": "2026-01-01T00:00:00"},
|
||||
)
|
||||
r = client.get(f"/health/medications/{mid}/usages")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
|
||||
|
||||
def test_update_usage(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.post(
|
||||
f"/health/medications/{mid}/usages",
|
||||
json={"valid_from": "2026-01-01T00:00:00"},
|
||||
)
|
||||
uid = r.json()["record_id"]
|
||||
|
||||
r2 = client.patch(f"/health/usages/{uid}", json={"dose_value": 2.5, "dose_unit": "mg"})
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["dose_value"] == 2.5
|
||||
assert r2.json()["dose_unit"] == "mg"
|
||||
|
||||
|
||||
def test_update_usage_not_found(client):
|
||||
r = client.patch(f"/health/usages/{uuid.uuid4()}", json={"dose_value": 1.0})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete_usage(client, created_medication):
|
||||
mid = created_medication["record_id"]
|
||||
r = client.post(
|
||||
f"/health/medications/{mid}/usages",
|
||||
json={"valid_from": "2026-01-01T00:00:00"},
|
||||
)
|
||||
uid = r.json()["record_id"]
|
||||
|
||||
r2 = client.delete(f"/health/usages/{uid}")
|
||||
assert r2.status_code == 204
|
||||
|
||||
|
||||
def test_delete_usage_not_found(client):
|
||||
r = client.delete(f"/health/usages/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lab results
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
LAB_RESULT_DATA = {
|
||||
"collected_at": "2026-01-15T09:00:00",
|
||||
"test_code": "718-7",
|
||||
"test_name_original": "Hemoglobin",
|
||||
"value_num": 14.5,
|
||||
"unit_original": "g/dL",
|
||||
"flag": "N",
|
||||
}
|
||||
|
||||
|
||||
def test_create_lab_result(client):
|
||||
r = client.post("/health/lab-results", json=LAB_RESULT_DATA)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert "record_id" in data
|
||||
assert data["test_code"] == "718-7"
|
||||
assert data["flag"] == "N"
|
||||
|
||||
|
||||
def test_create_lab_result_invalid_code(client):
|
||||
bad = {**LAB_RESULT_DATA, "test_code": "not-a-loinc-code"}
|
||||
r = client.post("/health/lab-results", json=bad)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_create_lab_result_invalid_flag(client):
|
||||
bad = {**LAB_RESULT_DATA, "flag": "INVALID"}
|
||||
r = client.post("/health/lab-results", json=bad)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_list_lab_results_empty(client):
|
||||
r = client.get("/health/lab-results")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
|
||||
def test_list_filter_test_code(client):
|
||||
client.post("/health/lab-results", json=LAB_RESULT_DATA)
|
||||
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "test_code": "2951-2"})
|
||||
r = client.get("/health/lab-results?test_code=718-7")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["test_code"] == "718-7"
|
||||
|
||||
|
||||
def test_list_filter_flag(client):
|
||||
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "flag": "N"})
|
||||
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "flag": "H"})
|
||||
r = client.get("/health/lab-results?flag=H")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["flag"] == "H"
|
||||
|
||||
|
||||
def test_list_filter_date_range(client):
|
||||
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "collected_at": "2026-01-01T00:00:00"})
|
||||
client.post("/health/lab-results", json={**LAB_RESULT_DATA, "collected_at": "2026-06-01T00:00:00"})
|
||||
r = client.get("/health/lab-results?from_date=2026-05-01T00:00:00")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
|
||||
|
||||
def test_get_lab_result(client):
|
||||
r = client.post("/health/lab-results", json=LAB_RESULT_DATA)
|
||||
rid = r.json()["record_id"]
|
||||
r2 = client.get(f"/health/lab-results/{rid}")
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["record_id"] == rid
|
||||
|
||||
|
||||
def test_get_lab_result_not_found(client):
|
||||
r = client.get(f"/health/lab-results/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_update_lab_result(client):
|
||||
r = client.post("/health/lab-results", json=LAB_RESULT_DATA)
|
||||
rid = r.json()["record_id"]
|
||||
r2 = client.patch(f"/health/lab-results/{rid}", json={"notes": "Recheck in 3 months"})
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["notes"] == "Recheck in 3 months"
|
||||
|
||||
|
||||
def test_delete_lab_result(client):
|
||||
r = client.post("/health/lab-results", json=LAB_RESULT_DATA)
|
||||
rid = r.json()["record_id"]
|
||||
r2 = client.delete(f"/health/lab-results/{rid}")
|
||||
assert r2.status_code == 204
|
||||
r3 = client.get(f"/health/lab-results/{rid}")
|
||||
assert r3.status_code == 404
|
||||
54
backend/tests/test_inventory.py
Normal file
54
backend/tests/test_inventory.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import uuid
|
||||
|
||||
|
||||
def test_get_inventory_by_id(client, created_product):
|
||||
pid = created_product["id"]
|
||||
r = client.post(f"/products/{pid}/inventory", json={"is_opened": False})
|
||||
assert r.status_code == 201
|
||||
inv_id = r.json()["id"]
|
||||
|
||||
r2 = client.get(f"/inventory/{inv_id}")
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["id"] == inv_id
|
||||
|
||||
|
||||
def test_get_inventory_not_found(client):
|
||||
r = client.get(f"/inventory/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_update_inventory_opened(client, created_product):
|
||||
pid = created_product["id"]
|
||||
r = client.post(f"/products/{pid}/inventory", json={"is_opened": False})
|
||||
inv_id = r.json()["id"]
|
||||
|
||||
r2 = client.patch(
|
||||
f"/inventory/{inv_id}",
|
||||
json={"is_opened": True, "opened_at": "2026-01-15"},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
data = r2.json()
|
||||
assert data["is_opened"] is True
|
||||
assert data["opened_at"] == "2026-01-15"
|
||||
|
||||
|
||||
def test_update_inventory_not_found(client):
|
||||
r = client.patch(f"/inventory/{uuid.uuid4()}", json={"is_opened": True})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete_inventory(client, created_product):
|
||||
pid = created_product["id"]
|
||||
r = client.post(f"/products/{pid}/inventory", json={})
|
||||
inv_id = r.json()["id"]
|
||||
|
||||
r2 = client.delete(f"/inventory/{inv_id}")
|
||||
assert r2.status_code == 204
|
||||
|
||||
r3 = client.get(f"/inventory/{inv_id}")
|
||||
assert r3.status_code == 404
|
||||
|
||||
|
||||
def test_delete_inventory_not_found(client):
|
||||
r = client.delete(f"/inventory/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
234
backend/tests/test_product_model.py
Normal file
234
backend/tests/test_product_model.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
"""Unit tests for Product.to_llm_context() — no database required."""
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from innercontext.models import Product
|
||||
from innercontext.models.enums import (
|
||||
DayTime,
|
||||
IngredientFunction,
|
||||
InteractionScope,
|
||||
ProductCategory,
|
||||
RoutineRole,
|
||||
)
|
||||
from innercontext.models.product import (
|
||||
ActiveIngredient,
|
||||
ProductContext,
|
||||
ProductEffectProfile,
|
||||
ProductInteraction,
|
||||
)
|
||||
|
||||
|
||||
def _make(**kwargs):
|
||||
defaults = dict(
|
||||
id=uuid4(),
|
||||
name="Test",
|
||||
brand="B",
|
||||
category=ProductCategory.MOISTURIZER,
|
||||
routine_role=RoutineRole.SEAL,
|
||||
recommended_time=DayTime.BOTH,
|
||||
leave_on=True,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Product(**defaults)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Always-present keys
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_always_present_keys():
|
||||
p = _make()
|
||||
ctx = p.to_llm_context()
|
||||
for key in ("id", "name", "brand", "category", "routine_role", "recommended_time", "leave_on"):
|
||||
assert key in ctx, f"Expected '{key}' in to_llm_context() output"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Optional string fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_optional_string_fields_absent_when_none():
|
||||
p = _make()
|
||||
ctx = p.to_llm_context()
|
||||
for key in ("line_name", "sku", "url", "barcode"):
|
||||
assert key not in ctx, f"'{key}' should not appear when None"
|
||||
|
||||
|
||||
def test_optional_string_fields_present_when_set():
|
||||
p = _make(line_name="Hydrating", sku="CV-001", url="https://example.com", barcode="123456")
|
||||
ctx = p.to_llm_context()
|
||||
assert ctx["line_name"] == "Hydrating"
|
||||
assert ctx["sku"] == "CV-001"
|
||||
assert ctx["url"] == "https://example.com"
|
||||
assert ctx["barcode"] == "123456"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# pH handling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ph_exact_collapses():
|
||||
p = _make(ph_min=5.5, ph_max=5.5)
|
||||
ctx = p.to_llm_context()
|
||||
assert "ph" in ctx
|
||||
assert ctx["ph"] == 5.5
|
||||
assert "ph_range" not in ctx
|
||||
assert "ph_min" not in ctx
|
||||
assert "ph_max" not in ctx
|
||||
|
||||
|
||||
def test_ph_range():
|
||||
p = _make(ph_min=4.0, ph_max=6.0)
|
||||
ctx = p.to_llm_context()
|
||||
assert "ph_range" in ctx
|
||||
assert "4.0" in ctx["ph_range"]
|
||||
assert "6.0" in ctx["ph_range"]
|
||||
assert "ph" not in ctx
|
||||
|
||||
|
||||
def test_ph_only_min():
|
||||
p = _make(ph_min=3.5, ph_max=None)
|
||||
ctx = p.to_llm_context()
|
||||
assert "ph_min" in ctx
|
||||
assert ctx["ph_min"] == 3.5
|
||||
assert "ph_range" not in ctx
|
||||
assert "ph" not in ctx
|
||||
|
||||
|
||||
def test_ph_only_max():
|
||||
p = _make(ph_min=None, ph_max=7.0)
|
||||
ctx = p.to_llm_context()
|
||||
assert "ph_max" in ctx
|
||||
assert ctx["ph_max"] == 7.0
|
||||
assert "ph_range" not in ctx
|
||||
assert "ph" not in ctx
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Actives
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_actives_pydantic_objects():
|
||||
ai = ActiveIngredient(
|
||||
name="Niacinamide",
|
||||
percent=10.0,
|
||||
functions=[IngredientFunction.NIACINAMIDE],
|
||||
)
|
||||
p = _make(actives=[ai])
|
||||
ctx = p.to_llm_context()
|
||||
assert "actives" in ctx
|
||||
a = ctx["actives"][0]
|
||||
assert a["name"] == "Niacinamide"
|
||||
assert a["percent"] == 10.0
|
||||
assert "niacinamide" in a["functions"]
|
||||
|
||||
|
||||
def test_actives_raw_dicts():
|
||||
raw = {"name": "Retinol", "percent": 0.1, "functions": ["retinoid"]}
|
||||
p = _make(actives=[raw])
|
||||
ctx = p.to_llm_context()
|
||||
assert "actives" in ctx
|
||||
assert ctx["actives"][0] == raw
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Effect profile
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_effect_profile_all_zeros_omitted():
|
||||
p = _make(product_effect_profile=ProductEffectProfile())
|
||||
ctx = p.to_llm_context()
|
||||
assert "effect_profile" not in ctx
|
||||
|
||||
|
||||
def test_effect_profile_nonzero_included():
|
||||
ep = ProductEffectProfile(hydration_immediate=4, barrier_repair_strength=3)
|
||||
p = _make(product_effect_profile=ep)
|
||||
ctx = p.to_llm_context()
|
||||
assert "effect_profile" in ctx
|
||||
assert ctx["effect_profile"]["hydration_immediate"] == 4
|
||||
assert ctx["effect_profile"]["barrier_repair_strength"] == 3
|
||||
# Zero fields should not be present
|
||||
assert "retinoid_strength" not in ctx["effect_profile"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incompatible_with
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_incompatible_with_pydantic_objects():
|
||||
inc = ProductInteraction(
|
||||
target="AHA", scope=InteractionScope.SAME_DAY, reason="increases irritation"
|
||||
)
|
||||
p = _make(incompatible_with=[inc])
|
||||
ctx = p.to_llm_context()
|
||||
assert "incompatible_with" in ctx
|
||||
assert ctx["incompatible_with"][0] == "avoid AHA (same_day): increases irritation"
|
||||
|
||||
|
||||
def test_incompatible_with_raw_dicts():
|
||||
raw = {"target": "Vitamin C", "scope": "same_step", "reason": None}
|
||||
p = _make(incompatible_with=[raw])
|
||||
ctx = p.to_llm_context()
|
||||
assert "incompatible_with" in ctx
|
||||
assert ctx["incompatible_with"][0] == "avoid Vitamin C (same_step)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Context rules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_context_rules_all_none_omitted():
|
||||
p = _make(context_rules=ProductContext())
|
||||
ctx = p.to_llm_context()
|
||||
assert "context_rules" not in ctx
|
||||
|
||||
|
||||
def test_context_rules_with_value():
|
||||
p = _make(context_rules=ProductContext(safe_after_shaving=True, low_uv_only=True))
|
||||
ctx = p.to_llm_context()
|
||||
assert "context_rules" in ctx
|
||||
assert ctx["context_rules"]["safe_after_shaving"] is True
|
||||
assert ctx["context_rules"]["low_uv_only"] is True
|
||||
assert "safe_after_acids" not in ctx["context_rules"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Safety dict
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_safety_dict_present_when_set():
|
||||
p = _make(fragrance_free=True, alcohol_denat_free=True)
|
||||
ctx = p.to_llm_context()
|
||||
assert "safety" in ctx
|
||||
assert ctx["safety"]["fragrance_free"] is True
|
||||
assert ctx["safety"]["alcohol_denat_free"] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Empty vs non-empty lists
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_empty_lists_omitted():
|
||||
p = _make(inci=[], targets=[])
|
||||
ctx = p.to_llm_context()
|
||||
assert "inci" not in ctx
|
||||
assert "targets" not in ctx
|
||||
|
||||
|
||||
def test_nonempty_lists_included():
|
||||
p = _make(inci=["Water", "Glycerin"], targets=["acne", "redness"])
|
||||
ctx = p.to_llm_context()
|
||||
assert "inci" in ctx
|
||||
assert ctx["inci"] == ["Water", "Glycerin"]
|
||||
assert "targets" in ctx
|
||||
198
backend/tests/test_products.py
Normal file
198
backend/tests/test_products.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
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
|
||||
219
backend/tests/test_routines.py
Normal file
219
backend/tests/test_routines.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import uuid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routines
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_routine_minimal(client):
|
||||
r = client.post(
|
||||
"/routines/", json={"routine_date": "2026-02-26", "part_of_day": "am"}
|
||||
)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert "id" in data
|
||||
assert data["routine_date"] == "2026-02-26"
|
||||
assert data["part_of_day"] == "am"
|
||||
|
||||
|
||||
def test_create_routine_invalid_part_of_day(client):
|
||||
r = client.post(
|
||||
"/routines/", json={"routine_date": "2026-02-26", "part_of_day": "noon"}
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_list_routines_empty(client):
|
||||
r = client.get("/routines/")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
|
||||
def test_list_filter_date_range(client):
|
||||
client.post("/routines/", json={"routine_date": "2026-01-01", "part_of_day": "am"})
|
||||
client.post("/routines/", json={"routine_date": "2026-06-01", "part_of_day": "am"})
|
||||
r = client.get("/routines/?from_date=2026-05-01&to_date=2026-12-31")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["routine_date"] == "2026-06-01"
|
||||
|
||||
|
||||
def test_list_filter_part_of_day(client):
|
||||
client.post("/routines/", json={"routine_date": "2026-02-01", "part_of_day": "am"})
|
||||
client.post("/routines/", json={"routine_date": "2026-02-02", "part_of_day": "pm"})
|
||||
r = client.get("/routines/?part_of_day=pm")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["part_of_day"] == "pm"
|
||||
|
||||
|
||||
def test_get_routine(client, created_routine):
|
||||
rid = created_routine["id"]
|
||||
r = client.get(f"/routines/{rid}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["id"] == rid
|
||||
|
||||
|
||||
def test_get_routine_not_found(client):
|
||||
r = client.get(f"/routines/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_update_routine_notes(client, created_routine):
|
||||
rid = created_routine["id"]
|
||||
r = client.patch(f"/routines/{rid}", json={"notes": "Felt great today"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["notes"] == "Felt great today"
|
||||
|
||||
|
||||
def test_update_routine_not_found(client):
|
||||
r = client.patch(f"/routines/{uuid.uuid4()}", json={"notes": "x"})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete_routine(client, created_routine):
|
||||
rid = created_routine["id"]
|
||||
r = client.delete(f"/routines/{rid}")
|
||||
assert r.status_code == 204
|
||||
r2 = client.get(f"/routines/{rid}")
|
||||
assert r2.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RoutineStep
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_add_step_action_only(client, created_routine):
|
||||
rid = created_routine["id"]
|
||||
r = client.post(
|
||||
f"/routines/{rid}/steps",
|
||||
json={"order_index": 0, "action_notes": "Cleanse face gently"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert data["order_index"] == 0
|
||||
assert data["action_notes"] == "Cleanse face gently"
|
||||
assert data["product_id"] is None
|
||||
|
||||
|
||||
def test_add_step_with_product(client, created_routine, created_product):
|
||||
rid = created_routine["id"]
|
||||
pid = created_product["id"]
|
||||
r = client.post(
|
||||
f"/routines/{rid}/steps",
|
||||
json={"order_index": 1, "product_id": pid},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert data["product_id"] == pid
|
||||
assert data["order_index"] == 1
|
||||
|
||||
|
||||
def test_add_step_routine_not_found(client):
|
||||
r = client.post(
|
||||
f"/routines/{uuid.uuid4()}/steps",
|
||||
json={"order_index": 0},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_update_step(client, created_routine):
|
||||
rid = created_routine["id"]
|
||||
r = client.post(
|
||||
f"/routines/{rid}/steps",
|
||||
json={"order_index": 0, "action_notes": "Initial"},
|
||||
)
|
||||
step_id = r.json()["id"]
|
||||
|
||||
r2 = client.patch(f"/routines/steps/{step_id}", json={"action_notes": "Updated"})
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["action_notes"] == "Updated"
|
||||
|
||||
|
||||
def test_update_step_not_found(client):
|
||||
r = client.patch(f"/routines/steps/{uuid.uuid4()}", json={"action_notes": "x"})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete_step(client, created_routine):
|
||||
rid = created_routine["id"]
|
||||
r = client.post(
|
||||
f"/routines/{rid}/steps",
|
||||
json={"order_index": 0},
|
||||
)
|
||||
step_id = r.json()["id"]
|
||||
|
||||
r2 = client.delete(f"/routines/steps/{step_id}")
|
||||
assert r2.status_code == 204
|
||||
|
||||
|
||||
def test_delete_step_not_found(client):
|
||||
r = client.delete(f"/routines/steps/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GroomingSchedule
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_list_grooming_schedule_empty(client):
|
||||
r = client.get("/routines/grooming-schedule")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
|
||||
def test_create_grooming_schedule(client):
|
||||
r = client.post(
|
||||
"/routines/grooming-schedule",
|
||||
json={"day_of_week": 1, "action": "shaving_razor"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert data["day_of_week"] == 1
|
||||
assert data["action"] == "shaving_razor"
|
||||
|
||||
|
||||
def test_list_grooming_schedule_returns_entry(client):
|
||||
client.post(
|
||||
"/routines/grooming-schedule",
|
||||
json={"day_of_week": 3, "action": "dermarolling"},
|
||||
)
|
||||
r = client.get("/routines/grooming-schedule")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) >= 1
|
||||
|
||||
|
||||
def test_update_grooming_schedule(client):
|
||||
r = client.post(
|
||||
"/routines/grooming-schedule",
|
||||
json={"day_of_week": 0, "action": "shaving_oneblade"},
|
||||
)
|
||||
entry_id = r.json()["id"]
|
||||
|
||||
r2 = client.patch(
|
||||
f"/routines/grooming-schedule/{entry_id}",
|
||||
json={"notes": "Use cold water"},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["notes"] == "Use cold water"
|
||||
|
||||
|
||||
def test_delete_grooming_schedule(client):
|
||||
r = client.post(
|
||||
"/routines/grooming-schedule",
|
||||
json={"day_of_week": 5, "action": "shaving_razor"},
|
||||
)
|
||||
entry_id = r.json()["id"]
|
||||
|
||||
r2 = client.delete(f"/routines/grooming-schedule/{entry_id}")
|
||||
assert r2.status_code == 204
|
||||
|
||||
|
||||
def test_delete_grooming_schedule_not_found(client):
|
||||
r = client.delete(f"/routines/grooming-schedule/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
132
backend/tests/test_skincare.py
Normal file
132
backend/tests/test_skincare.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import uuid
|
||||
|
||||
|
||||
def test_create_snapshot_minimal(client):
|
||||
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert "id" in data
|
||||
assert data["snapshot_date"] == "2026-02-26"
|
||||
assert data["overall_state"] is None
|
||||
|
||||
|
||||
def test_create_snapshot_full(client):
|
||||
r = client.post(
|
||||
"/skin-snapshots/",
|
||||
json={
|
||||
"snapshot_date": "2026-02-20",
|
||||
"overall_state": "good",
|
||||
"trend": "improving",
|
||||
"skin_type": "combination",
|
||||
"hydration_level": 3,
|
||||
"sebum_tzone": 4,
|
||||
"sebum_cheeks": 2,
|
||||
"sensitivity_level": 2,
|
||||
"barrier_state": "intact",
|
||||
"active_concerns": ["acne", "redness"],
|
||||
"risks": ["Over-exfoliation"],
|
||||
"priorities": ["Maintain barrier"],
|
||||
"notes": "Looking better this week",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
data = r.json()
|
||||
assert data["overall_state"] == "good"
|
||||
assert data["trend"] == "improving"
|
||||
assert "acne" in data["active_concerns"]
|
||||
assert data["hydration_level"] == 3
|
||||
|
||||
|
||||
def test_create_snapshot_invalid_state(client):
|
||||
r = client.post(
|
||||
"/skin-snapshots/",
|
||||
json={"snapshot_date": "2026-02-26", "overall_state": "stellar"},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_list_snapshots_empty(client):
|
||||
r = client.get("/skin-snapshots/")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
|
||||
def test_list_filter_date_range(client):
|
||||
client.post("/skin-snapshots/", json={"snapshot_date": "2026-01-01"})
|
||||
client.post("/skin-snapshots/", json={"snapshot_date": "2026-06-01"})
|
||||
r = client.get("/skin-snapshots/?from_date=2026-05-01&to_date=2026-12-31")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["snapshot_date"] == "2026-06-01"
|
||||
|
||||
|
||||
def test_list_filter_overall_state(client):
|
||||
client.post(
|
||||
"/skin-snapshots/",
|
||||
json={"snapshot_date": "2026-02-10", "overall_state": "good"},
|
||||
)
|
||||
client.post(
|
||||
"/skin-snapshots/",
|
||||
json={"snapshot_date": "2026-02-11", "overall_state": "poor"},
|
||||
)
|
||||
r = client.get("/skin-snapshots/?overall_state=poor")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["overall_state"] == "poor"
|
||||
|
||||
|
||||
def test_get_snapshot(client):
|
||||
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
|
||||
sid = r.json()["id"]
|
||||
r2 = client.get(f"/skin-snapshots/{sid}")
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["id"] == sid
|
||||
|
||||
|
||||
def test_get_snapshot_not_found(client):
|
||||
r = client.get(f"/skin-snapshots/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_update_snapshot_state(client):
|
||||
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
|
||||
sid = r.json()["id"]
|
||||
r2 = client.patch(f"/skin-snapshots/{sid}", json={"overall_state": "excellent"})
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["overall_state"] == "excellent"
|
||||
|
||||
|
||||
def test_update_snapshot_concerns(client):
|
||||
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
|
||||
sid = r.json()["id"]
|
||||
r2 = client.patch(
|
||||
f"/skin-snapshots/{sid}",
|
||||
json={"active_concerns": ["dehydration", "redness"]},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
data = r2.json()
|
||||
assert "dehydration" in data["active_concerns"]
|
||||
assert "redness" in data["active_concerns"]
|
||||
|
||||
|
||||
def test_update_snapshot_not_found(client):
|
||||
r = client.patch(
|
||||
f"/skin-snapshots/{uuid.uuid4()}", json={"overall_state": "good"}
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_delete_snapshot(client):
|
||||
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
|
||||
sid = r.json()["id"]
|
||||
r2 = client.delete(f"/skin-snapshots/{sid}")
|
||||
assert r2.status_code == 204
|
||||
r3 = client.get(f"/skin-snapshots/{sid}")
|
||||
assert r3.status_code == 404
|
||||
|
||||
|
||||
def test_delete_snapshot_not_found(client):
|
||||
r = client.delete(f"/skin-snapshots/{uuid.uuid4()}")
|
||||
assert r.status_code == 404
|
||||
Loading…
Add table
Add a link
Reference in a new issue