fix: resolve frontend/backend integration bugs

- Rename skincare route prefix /skin-snapshots → /skincare to match API client
- Add redirect_slashes=False to FastAPI app; change collection routes from "/" to ""
  to eliminate 307 redirects on POST/GET without trailing slash
- Fix redirect() inside try/catch in products/new and routines/new server actions
  (SvelteKit redirect() throws and was being caught as a 500 error)
- Eagerly load inventory and steps relationships via explicit SELECT + model_dump(mode="json"),
  working around SQLModel 0.0.37 not serializing Relationship fields in response_model
- Add field_validator for product_effect_profile to coerce DB-returned dict → ProductEffectProfile,
  eliminating Pydantic serializer warning
- Update all tests to use routes without trailing slash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-02-26 21:53:17 +01:00
parent 8d4f9d1fc6
commit 9bf94a979c
11 changed files with 85 additions and 68 deletions

View file

@ -61,7 +61,7 @@ def product_data():
@pytest.fixture()
def created_product(client, product_data):
r = client.post("/products/", json=product_data)
r = client.post("/products", json=product_data)
assert r.status_code == 201
return r.json()
@ -81,7 +81,7 @@ def created_medication(client, medication_data):
@pytest.fixture()
def created_routine(client):
r = client.post(
"/routines/", json={"routine_date": "2026-02-26", "part_of_day": "am"}
"/routines", json={"routine_date": "2026-02-26", "part_of_day": "am"}
)
assert r.status_code == 201
return r.json()

View file

@ -2,7 +2,7 @@ import uuid
def test_create_minimal(client, product_data):
r = client.post("/products/", json=product_data)
r = client.post("/products", json=product_data)
assert r.status_code == 201
data = r.json()
assert "id" in data
@ -15,7 +15,7 @@ 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)
r = client.post("/products", json=product_data)
assert r.status_code == 201
data = r.json()
assert len(data["actives"]) == 1
@ -25,24 +25,24 @@ def test_create_with_actives(client, product_data):
def test_create_invalid_enum(client, product_data):
product_data["category"] = "not_a_category"
r = client.post("/products/", json=product_data)
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)
r = client.post("/products", json=product_data)
assert r.status_code == 422
def test_list_empty(client):
r = client.get("/products/")
r = client.get("/products")
assert r.status_code == 200
assert r.json() == []
def test_list_returns_created(client, created_product):
r = client.get("/products/")
r = client.get("/products")
assert r.status_code == 200
ids = [p["id"] for p in r.json()]
assert created_product["id"] in ids
@ -56,12 +56,12 @@ def test_list_filter_category(client, client_and_data=None):
"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"})
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")
r = client.get("/products?category=moisturizer")
assert r.status_code == 200
data = r.json()
assert all(p["category"] == "moisturizer" for p in data)
@ -77,10 +77,10 @@ def test_list_filter_brand(client):
"leave_on": True,
"category": "serum",
}
client.post("/products/", json={**base, "name": "A1", "brand": "BrandA"})
client.post("/products/", json={**base, "name": "B1", "brand": "BrandB"})
client.post("/products", json={**base, "name": "A1", "brand": "BrandA"})
client.post("/products", json={**base, "name": "B1", "brand": "BrandB"})
r = client.get("/products/?brand=BrandA")
r = client.get("/products?brand=BrandA")
assert r.status_code == 200
data = r.json()
assert all(p["brand"] == "BrandA" for p in data)
@ -95,14 +95,14 @@ def test_list_filter_is_medication(client):
"leave_on": True,
"category": "serum",
}
client.post("/products/", json={**base, "name": "Normal", "is_medication": False})
client.post("/products", json={**base, "name": "Normal", "is_medication": False})
# is_medication=True requires usage_notes (model validator)
client.post(
"/products/",
"/products",
json={**base, "name": "Med", "is_medication": True, "usage_notes": "Apply pea-sized amount"},
)
r = client.get("/products/?is_medication=true")
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)
@ -118,10 +118,10 @@ def test_list_filter_targets(client):
"leave_on": True,
"category": "serum",
}
client.post("/products/", json={**base, "name": "Acne", "targets": ["acne"]})
client.post("/products/", json={**base, "name": "Aging", "targets": ["aging"]})
client.post("/products", json={**base, "name": "Acne", "targets": ["acne"]})
client.post("/products", json={**base, "name": "Aging", "targets": ["aging"]})
r = client.get("/products/?targets=acne")
r = client.get("/products?targets=acne")
assert r.status_code == 200
data = r.json()
assert len(data) == 1

View file

@ -8,7 +8,7 @@ import uuid
def test_create_routine_minimal(client):
r = client.post(
"/routines/", json={"routine_date": "2026-02-26", "part_of_day": "am"}
"/routines", json={"routine_date": "2026-02-26", "part_of_day": "am"}
)
assert r.status_code == 201
data = r.json()
@ -19,21 +19,21 @@ def test_create_routine_minimal(client):
def test_create_routine_invalid_part_of_day(client):
r = client.post(
"/routines/", json={"routine_date": "2026-02-26", "part_of_day": "noon"}
"/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/")
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")
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
@ -41,9 +41,9 @@ def test_list_filter_date_range(client):
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")
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

View file

@ -2,7 +2,7 @@ import uuid
def test_create_snapshot_minimal(client):
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
r = client.post("/skincare", json={"snapshot_date": "2026-02-26"})
assert r.status_code == 201
data = r.json()
assert "id" in data
@ -12,7 +12,7 @@ def test_create_snapshot_minimal(client):
def test_create_snapshot_full(client):
r = client.post(
"/skin-snapshots/",
"/skincare",
json={
"snapshot_date": "2026-02-20",
"overall_state": "good",
@ -39,22 +39,22 @@ def test_create_snapshot_full(client):
def test_create_snapshot_invalid_state(client):
r = client.post(
"/skin-snapshots/",
"/skincare",
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/")
r = client.get("/skincare")
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")
client.post("/skincare", json={"snapshot_date": "2026-01-01"})
client.post("/skincare", json={"snapshot_date": "2026-06-01"})
r = client.get("/skincare?from_date=2026-05-01&to_date=2026-12-31")
assert r.status_code == 200
data = r.json()
assert len(data) == 1
@ -63,14 +63,14 @@ def test_list_filter_date_range(client):
def test_list_filter_overall_state(client):
client.post(
"/skin-snapshots/",
"/skincare",
json={"snapshot_date": "2026-02-10", "overall_state": "good"},
)
client.post(
"/skin-snapshots/",
"/skincare",
json={"snapshot_date": "2026-02-11", "overall_state": "poor"},
)
r = client.get("/skin-snapshots/?overall_state=poor")
r = client.get("/skincare?overall_state=poor")
assert r.status_code == 200
data = r.json()
assert len(data) == 1
@ -78,31 +78,31 @@ def test_list_filter_overall_state(client):
def test_get_snapshot(client):
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
r = client.post("/skincare", json={"snapshot_date": "2026-02-26"})
sid = r.json()["id"]
r2 = client.get(f"/skin-snapshots/{sid}")
r2 = client.get(f"/skincare/{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()}")
r = client.get(f"/skincare/{uuid.uuid4()}")
assert r.status_code == 404
def test_update_snapshot_state(client):
r = client.post("/skin-snapshots/", json={"snapshot_date": "2026-02-26"})
r = client.post("/skincare", json={"snapshot_date": "2026-02-26"})
sid = r.json()["id"]
r2 = client.patch(f"/skin-snapshots/{sid}", json={"overall_state": "excellent"})
r2 = client.patch(f"/skincare/{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"})
r = client.post("/skincare", json={"snapshot_date": "2026-02-26"})
sid = r.json()["id"]
r2 = client.patch(
f"/skin-snapshots/{sid}",
f"/skincare/{sid}",
json={"active_concerns": ["dehydration", "redness"]},
)
assert r2.status_code == 200
@ -113,20 +113,20 @@ def test_update_snapshot_concerns(client):
def test_update_snapshot_not_found(client):
r = client.patch(
f"/skin-snapshots/{uuid.uuid4()}", json={"overall_state": "good"}
f"/skincare/{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"})
r = client.post("/skincare", json={"snapshot_date": "2026-02-26"})
sid = r.json()["id"]
r2 = client.delete(f"/skin-snapshots/{sid}")
r2 = client.delete(f"/skincare/{sid}")
assert r2.status_code == 204
r3 = client.get(f"/skin-snapshots/{sid}")
r3 = client.get(f"/skincare/{sid}")
assert r3.status_code == 404
def test_delete_snapshot_not_found(client):
r = client.delete(f"/skin-snapshots/{uuid.uuid4()}")
r = client.delete(f"/skincare/{uuid.uuid4()}")
assert r.status_code == 404