2 Commits

Author SHA1 Message Date
Adriano fb96bad000 fix(tests): align stale tests with current API + bypass identity-map cache
- test_recipes: split update test into in-place vs copy-on-write paths
  (PUT now updates in-place when current version has no measurements).
  Added _seed_measurement() helper to force copy-on-write where needed.
- test_tasks::reorder: expunge_all() between POST and GET so the shared
  test session re-fetches RecipeVersion.tasks instead of returning the
  cached collection. Prod uses one session per request, so identity-map
  staleness only affects tests.
- test_measure::save_measurement_proxy: payload aligned to current API
  (version_id is required; pass_fail/deviation are computed server-side).

All 179 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:07:32 +02:00
Adriano 6a8931e108 fix(i18n): aggiunge traduzioni EN mancanti per GUI /admin/stations
Le ~42 stringhe di admin/stations.html e navbar (Stazioni, Postazione,
Gestione Stazioni, ...) non erano state estratte nel catalogo .pot dopo
l'aggiunta della GUI, quindi in EN si vedeva ancora italiano via fallback
msgid. pybabel update aveva poi auto-matchato alcune chiavi simili come
"fuzzy", producendo traduzioni assurde (Postazione → Settings, ecc.) che
Babel ignora a runtime.

Rigenerato messages.pot via pybabel extract, applicate traduzioni EN
corrette per tutte le entry fuzzy, rimossi flag fuzzy. IT lasciato con
msgstr vuoto (fallback su msgid = italiano nativo).

Tech debt: serve un test guard che faccia fail se pybabel extract produce
diff vs messages.pot committato.
2026-05-11 18:13:20 +02:00
6 changed files with 2201 additions and 1382 deletions
+76 -18
View File
@@ -1,12 +1,40 @@
"""Tests for recipes router (/api/recipes)."""
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.models.orm.measurement import Measurement
from src.backend.models.orm.recipe import RecipeVersion
from src.backend.models.orm.task import RecipeSubtask
from src.backend.models.orm.user import User
from src.backend.tests.conftest import auth_headers, create_test_recipe
async def _seed_measurement(db: AsyncSession, recipe_id: int, user_id: int) -> None:
version_id = (await db.execute(
select(RecipeVersion.id).where(
RecipeVersion.recipe_id == recipe_id,
RecipeVersion.is_current == True, # noqa: E712
)
)).scalar_one()
subtask_id = (await db.execute(
select(RecipeSubtask.id)
.join(RecipeSubtask.task)
.where(RecipeSubtask.task.has(version_id=version_id))
)).scalar_one()
db.add(Measurement(
subtask_id=subtask_id,
version_id=version_id,
measured_by=user_id,
value=10.0,
pass_fail="pass",
deviation=0.0,
input_method="manual",
))
await db.flush()
class TestListRecipes:
"""GET /api/recipes tests."""
@@ -160,18 +188,13 @@ class TestCreateRecipe:
class TestUpdateRecipe:
"""PUT /api/recipes/{recipe_id} tests."""
async def test_update_recipe(
async def test_update_recipe_in_place(
self,
client: AsyncClient,
maker_user: User,
db_session: AsyncSession,
):
"""Updating a recipe creates a new version (copy-on-write).
We verify the new version exists by listing versions, since
SQLAlchemy identity-map staleness with StaticPool can cause
``is_current`` to appear stale in some responses.
"""
"""Updating a recipe without measurements updates current version in-place."""
recipe = await create_test_recipe(db_session, maker_user.id)
resp = await client.put(
f"/api/recipes/{recipe.id}",
@@ -183,7 +206,9 @@ class TestUpdateRecipe:
)
assert resp.status_code == 200
# Verify via versions list which always does a clean query
# expire identity-map cache so subsequent GETs see fresh data
db_session.expunge_all()
versions_resp = await client.get(
f"/api/recipes/{recipe.id}/versions",
headers=auth_headers(maker_user),
@@ -191,8 +216,38 @@ class TestUpdateRecipe:
assert versions_resp.status_code == 200
versions = versions_resp.json()
version_numbers = [v["version_number"] for v in versions]
assert 1 in version_numbers
assert 2 in version_numbers
assert version_numbers == [1]
async def test_update_recipe_copy_on_write(
self,
client: AsyncClient,
maker_user: User,
db_session: AsyncSession,
):
"""Updating a recipe with measurements creates a new version (copy-on-write)."""
recipe = await create_test_recipe(db_session, maker_user.id)
await _seed_measurement(db_session, recipe.id, maker_user.id)
db_session.expunge_all()
resp = await client.put(
f"/api/recipes/{recipe.id}",
headers=auth_headers(maker_user),
json={
"name": "Updated Recipe",
"change_notes": "Triggers copy-on-write",
},
)
assert resp.status_code == 200
db_session.expunge_all()
versions_resp = await client.get(
f"/api/recipes/{recipe.id}/versions",
headers=auth_headers(maker_user),
)
assert versions_resp.status_code == 200
versions = versions_resp.json()
version_numbers = sorted(v["version_number"] for v in versions)
assert version_numbers == [1, 2]
class TestDeleteRecipe:
@@ -222,31 +277,34 @@ class TestRecipeVersioning:
maker_user: User,
db_session: AsyncSession,
):
"""Multiple updates create consecutive versions."""
"""Each update creates a new version when measurements exist (copy-on-write)."""
recipe = await create_test_recipe(db_session, maker_user.id)
await _seed_measurement(db_session, recipe.id, maker_user.id)
db_session.expunge_all()
# Update once -> v2
# Update once -> v2 (v1 had measurements)
await client.put(
f"/api/recipes/{recipe.id}",
headers=auth_headers(maker_user),
json={"change_notes": "Version 2"},
)
db_session.expunge_all()
await _seed_measurement(db_session, recipe.id, maker_user.id)
db_session.expunge_all()
# Update again -> v3
# Update again -> v3 (v2 now has measurements too)
await client.put(
f"/api/recipes/{recipe.id}",
headers=auth_headers(maker_user),
json={"change_notes": "Version 3"},
)
db_session.expunge_all()
# List versions
resp = await client.get(
f"/api/recipes/{recipe.id}/versions",
headers=auth_headers(maker_user),
)
assert resp.status_code == 200
versions = resp.json()
version_numbers = [v["version_number"] for v in versions]
assert 1 in version_numbers
assert 2 in version_numbers
assert 3 in version_numbers
version_numbers = sorted(v["version_number"] for v in versions)
assert version_numbers == [1, 2, 3]
+6 -2
View File
@@ -189,7 +189,7 @@ class TestReorderTasks:
"""Maker can reorder tasks by providing task IDs in desired order."""
recipe = await create_test_recipe(db_session, maker_user.id)
# Add a second task via copy-on-write
# Add a second task (no measurements → appended to current version)
add_resp = await client.post(
f"/api/recipes/{recipe.id}/tasks",
headers=auth_headers(maker_user),
@@ -197,7 +197,11 @@ class TestReorderTasks:
)
assert add_resp.status_code == 201
# Get tasks from new version
# Force SQLA to re-fetch RecipeVersion.tasks: the shared test session
# caches the collection from create_test_recipe, so the GET below would
# otherwise return a stale list. In prod each request gets its own session.
db_session.expunge_all()
tasks_resp = await client.get(
f"/api/recipes/{recipe.id}/tasks",
headers=auth_headers(maker_user),
+3 -5
View File
@@ -61,7 +61,7 @@ class TestSaveMeasurement:
mock_api_client.post.return_value = {
"id": 1,
"subtask_id": 10,
"task_id": 5,
"version_id": 3,
"value": 9.95,
"pass_fail": "pass",
}
@@ -70,10 +70,8 @@ class TestSaveMeasurement:
"/measure/save-measurement",
json={
"subtask_id": 10,
"task_id": 5,
"version_id": 3,
"value": 9.95,
"pass_fail": "pass",
"deviation": 0.05,
},
content_type="application/json",
)
@@ -86,7 +84,7 @@ class TestSaveMeasurement:
"""Missing required fields return 400."""
resp = logged_in_client.post(
"/measure/save-measurement",
json={"subtask_id": 10}, # missing task_id and value
json={"subtask_id": 10}, # missing version_id and value
content_type="application/json",
)
assert resp.status_code == 400
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff