Files
TieMeasureFlow/src/frontend/flask_app/tests/test_maker.py
T
Adriano b27d56d4f7 fix(maker): l'editor task si riallinea al copy-on-write invece di 409 (#4)
Dopo il copy-on-write del backend (modifica su versione con misure ->
nuovi id per task e quote), task_editor.html teneva in memoria gli id
vecchi: il salvataggio successivo riceveva 409 "Cannot modify a
non-current version". Ogni handler di salvataggio (create/update/delete
di task e quote, reorder, upload immagine) ora ricarica la pagina su
409, mostrando un avviso dopo il reload. updateTask/updateSubtask/
addSubtask ricaricano anche su successo se l'id restituito (recipe_id
task, task_id quota) non corrisponde a quello locale - il copy-on-write
avvenuto silenziosamente.

Stringa aggiunta ai due cataloghi .po. Test: proxy Flask che verifica
il passaggio del 409 (test_maker.py), sintassi JS dello script inline
di task_editor.html con node --check (test_template_js_syntax.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:33:01 +02:00

164 lines
5.9 KiB
Python

"""Tests for maker blueprint (client/blueprints/maker.py).
Covers recipe list, recipe creation page, role enforcement, and recipe edit.
"""
import pytest
class TestRecipeList:
"""GET /maker/recipes tests."""
def test_recipe_list_renders(self, logged_in_client, mock_api_client):
"""Recipe list page renders for Maker role."""
mock_api_client.get.return_value = {
"items": [
{"id": 1, "code": "REC-001", "name": "Recipe A"},
],
"total": 1,
"pages": 1,
}
resp = logged_in_client.get("/maker/recipes")
assert resp.status_code == 200
def test_recipe_list_requires_login(self, client):
"""Unauthenticated user is redirected to login."""
resp = client.get("/maker/recipes", follow_redirects=False)
assert resp.status_code == 302
assert "/auth/login" in resp.headers["Location"]
def test_recipe_list_search_does_not_break_out_of_x_data(
self, logged_in_client, mock_api_client
):
"""#10: ``?search=`` is JSON-encoded inside x-data, not string-interpolated.
The payload must land as the *content* of a single quoted JSON
string token (recovered by undoing only the ``\\u0027`` escape),
and the object key right after it must still be the untouched
``filter: 'all'`` from the template - proof no new key was injected.
"""
import re
from html.parser import HTMLParser
mock_api_client.get.return_value = {"items": [], "total": 0, "pages": 1}
payload = "',x:alert(1),y:'"
resp = logged_in_client.get("/maker/recipes", query_string={"search": payload})
assert resp.status_code == 200
html = resp.get_data(as_text=True)
captured = {}
class _P(HTMLParser):
def handle_starttag(self, tag, attrs):
d = dict(attrs)
if d.get("x-data") and "search:" in d["x-data"]:
captured["x_data"] = d["x-data"]
_P().feed(html)
assert "x_data" in captured
m = re.search(r'search:\s*"([^"]*)"\s*,\s*filter:\s*\'all\'', captured["x_data"])
assert m, f"search is not a single quoted JSON token: {captured['x_data']!r}"
assert m.group(1).replace("\\u0027", "'") == payload
class TestRecipeNew:
"""GET /maker/recipes/new tests."""
def test_recipe_new_renders(self, logged_in_client, mock_api_client):
"""Recipe creation page renders for Maker role."""
resp = logged_in_client.get("/maker/recipes/new")
assert resp.status_code == 200
class TestRoleRequired:
"""Maker role enforcement tests."""
def test_requires_maker_role(self, flask_app):
"""User without Maker role gets 403 on maker endpoints."""
with flask_app.test_client() as c:
with c.session_transaction() as sess:
sess["api_key"] = "test-key"
sess["user"] = {
"id": 2,
"username": "tec_only",
"display_name": "Tec Only",
"roles": ["MeasurementTec"],
"is_admin": False,
"language_pref": "en",
"theme_pref": "light",
"active": True,
}
sess["user_id"] = 2
sess["language"] = "en"
sess["theme"] = "light"
resp = c.get("/maker/recipes")
assert resp.status_code == 403
class TestRecipeEdit:
"""GET /maker/recipes/<id>/edit tests."""
def test_recipe_edit_renders(self, logged_in_client, mock_api_client):
"""Recipe edit page renders with recipe data."""
# First call: recipe detail, second call: versions list
mock_api_client.get.side_effect = [
{"id": 1, "code": "REC-001", "name": "Recipe A"},
[{"version_number": 1, "is_current": True}],
]
resp = logged_in_client.get("/maker/recipes/1/edit")
assert resp.status_code == 200
def test_recipe_edit_not_found(self, logged_in_client, mock_api_client):
"""Recipe edit with API error redirects to recipe list."""
mock_api_client.get.return_value = {
"error": True,
"detail": "Not found",
}
resp = logged_in_client.get("/maker/recipes/999/edit", follow_redirects=False)
assert resp.status_code == 302
assert "/maker/recipes" in resp.headers["Location"]
class TestVersionConflictProxy:
"""gitea#4: a 409 from the backend (edit targeting a non-current version,
left behind by an earlier copy-on-write) must reach the browser as a 409 -
task_editor.html's _reloadForNewVersion() only triggers on that status.
"""
_CONFLICT = {
"error": True,
"status_code": 409,
"detail": "Cannot modify a non-current version. Create a new version first.",
}
def test_update_task_proxies_409(self, logged_in_client, mock_api_client):
mock_api_client.put.return_value = self._CONFLICT
resp = logged_in_client.put("/maker/api/tasks/11", json={"title": "x"})
assert resp.status_code == 409
assert resp.get_json()["detail"] == self._CONFLICT["detail"]
def test_update_subtask_proxies_409(self, logged_in_client, mock_api_client):
mock_api_client.put.return_value = self._CONFLICT
resp = logged_in_client.put("/maker/api/subtasks/21", json={"nominal": 1.0})
assert resp.status_code == 409
assert resp.get_json()["detail"] == self._CONFLICT["detail"]
def test_create_subtask_proxies_409(self, logged_in_client, mock_api_client):
mock_api_client.post.return_value = self._CONFLICT
resp = logged_in_client.post(
"/maker/api/tasks/11/subtasks",
json={"marker_number": 1, "description": "x"},
)
assert resp.status_code == 409
assert resp.get_json()["detail"] == self._CONFLICT["detail"]