fix(measure): filtro stazione anche su barcode, avvio sequenza e task_execute (#24)
_recipe_allowed_at_station() riusa get_station_recipes() (stessa fonte di /measure/select) per lookup_barcode, start_sequence e task_execute: prima solo l'elenco filtrava, il barcode e i link diretti lasciavano misurare ricette non assegnate alla stazione. Ricetta non assegnata -> abort(403) con messaggio "ricetta non assegnata a questa stazione" (pagine, come già fa role_required) o JSON 403 (lookup-barcode). Aggiornati i test esistenti che ora richiedono get_station_recipes/STATION_CODE mockati. Backend (ruoli sui run, StationRecipeAssignment su open_run) resta al lotto A1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -170,7 +170,15 @@ class TestTaskList:
|
||||
class TestStartSequence:
|
||||
"""GET /measure/start/<recipe_id> — the operator's way in."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _station_configured(self, monkeypatch):
|
||||
# start_sequence() checks recipe-station assignment (#24); STATION_CODE
|
||||
# defaults to unset in the test env, so every test in this class needs it.
|
||||
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||
|
||||
def test_start_goes_to_the_first_task(self, logged_in_client, mock_api_client):
|
||||
# start_sequence() only accepts a recipe assigned to this station (#24).
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/recipes/1/tasks": [
|
||||
{"id": 31, "title": "Nota", "order_index": 1},
|
||||
@@ -186,6 +194,7 @@ class TestStartSequence:
|
||||
def test_start_remembers_the_traceability(self, logged_in_client, mock_api_client):
|
||||
"""The list used to be where lot and serial were stored; the sequence
|
||||
skips it, so the storing has to happen on the way in."""
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/recipes/1/tasks": [{"id": 30, "title": "Prima", "order_index": 0}],
|
||||
"/api/recipes/1": {"id": 1, "code": "REC-001", "name": "Test Recipe"},
|
||||
@@ -200,6 +209,7 @@ class TestStartSequence:
|
||||
self, logged_in_client, mock_api_client,
|
||||
):
|
||||
"""Point 8 holds on this door too: it is a way into the measurement."""
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/recipes/1/tasks": [{"id": 30, "title": "Prima", "order_index": 0}],
|
||||
"/api/recipes/1": {
|
||||
@@ -215,6 +225,7 @@ class TestStartSequence:
|
||||
def test_start_without_tasks_falls_back_to_the_list(
|
||||
self, logged_in_client, mock_api_client,
|
||||
):
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/recipes/1/tasks": [],
|
||||
"/api/recipes/1": {"id": 1, "code": "REC-001", "name": "Test Recipe"},
|
||||
@@ -224,6 +235,18 @@ class TestStartSequence:
|
||||
assert resp.status_code == 302
|
||||
assert resp.headers["Location"].endswith("/measure/tasks/1")
|
||||
|
||||
def test_start_refuses_recipe_not_assigned_to_station(
|
||||
self, logged_in_client, mock_api_client,
|
||||
):
|
||||
"""#24: a recipe not on this station's list cannot be started via a direct link."""
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 99}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/recipes/1": {"id": 1, "code": "REC-001", "name": "Test Recipe"},
|
||||
})
|
||||
|
||||
resp = logged_in_client.get("/measure/start/1")
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
class TestTaskComplete:
|
||||
"""GET /measure/complete/<recipe_id> tests."""
|
||||
|
||||
@@ -4,6 +4,8 @@ from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import api_get_router
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _restore_measure_module():
|
||||
@@ -151,3 +153,66 @@ def test_station_switch_normalises_case(logged_in_client, monkeypatch):
|
||||
mock_api.get_station_recipes.return_value = []
|
||||
logged_in_client.get("/measure/select?station=st-other")
|
||||
assert _station_calls(mock_api) == ["ST-OTHER", "ST-OTHER"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# gitea#24: barcode lookup, the guided-sequence shortcut and a direct link
|
||||
# into task_execute must not let an operator bypass the picker's station
|
||||
# filter just by knowing (or scanning) a recipe's code/id.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_lookup_barcode_rejects_recipe_not_assigned_to_station(logged_in_client, monkeypatch):
|
||||
_reload_measure(monkeypatch, station_code="ST-TEST")
|
||||
from blueprints import measure as measure_mod
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.get.side_effect = api_get_router({
|
||||
"/api/recipes/code/R-OTHER": {"id": 99, "code": "R-OTHER", "name": "Other"},
|
||||
})
|
||||
mock_api.get_station_recipes.return_value = [{"id": 1, "code": "R1"}]
|
||||
|
||||
resp = logged_in_client.post("/measure/lookup-barcode", json={"code": "R-OTHER"})
|
||||
assert resp.status_code == 403
|
||||
body = resp.get_json()
|
||||
assert body["error"] is True
|
||||
assert "assegnata" in body["detail"].lower()
|
||||
|
||||
|
||||
def test_lookup_barcode_accepts_recipe_assigned_to_station(logged_in_client, monkeypatch):
|
||||
_reload_measure(monkeypatch, station_code="ST-TEST")
|
||||
from blueprints import measure as measure_mod
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.get.side_effect = api_get_router({
|
||||
"/api/recipes/code/R1": {"id": 1, "code": "R1", "name": "Assigned"},
|
||||
})
|
||||
mock_api.get_station_recipes.return_value = [{"id": 1, "code": "R1"}]
|
||||
|
||||
resp = logged_in_client.post("/measure/lookup-barcode", json={"code": "R1"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["id"] == 1
|
||||
|
||||
|
||||
def test_start_sequence_rejects_recipe_not_assigned_to_station(logged_in_client, monkeypatch):
|
||||
_reload_measure(monkeypatch, station_code="ST-TEST")
|
||||
from blueprints import measure as measure_mod
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.get.side_effect = api_get_router({
|
||||
"/api/recipes/99": {"id": 99, "code": "R-OTHER", "name": "Other"},
|
||||
})
|
||||
mock_api.get_station_recipes.return_value = [{"id": 1, "code": "R1"}]
|
||||
|
||||
resp = logged_in_client.get("/measure/start/99")
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_task_execute_rejects_recipe_not_assigned_to_station(logged_in_client, monkeypatch):
|
||||
_reload_measure(monkeypatch, station_code="ST-TEST")
|
||||
from blueprints import measure as measure_mod
|
||||
with patch.object(measure_mod, "api_client") as mock_api:
|
||||
mock_api.get.side_effect = api_get_router({
|
||||
"/api/tasks/10": {"id": 10, "recipe_id": 99, "subtasks": []},
|
||||
})
|
||||
mock_api.get_station_recipes.return_value = [{"id": 1, "code": "R1"}]
|
||||
|
||||
resp = logged_in_client.get("/measure/execute/10")
|
||||
assert resp.status_code == 403
|
||||
|
||||
@@ -47,8 +47,14 @@ def _operator_only(client):
|
||||
sess["user"] = user
|
||||
|
||||
|
||||
def _execute_page(client, mock_api_client):
|
||||
def _execute_page(client, mock_api_client, monkeypatch):
|
||||
_italian(client)
|
||||
# task_execute() only renders a recipe assigned to this station (#24).
|
||||
# STATION_CODE defaults to unset in the test env, so it has to be forced
|
||||
# here for the same reason test_the_selection_screen_starts_the_sequence
|
||||
# forces it below.
|
||||
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 3}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/tasks/11": TASK,
|
||||
"/api/recipes/3/tasks": [{"id": 11, "order_index": 0}],
|
||||
@@ -147,9 +153,9 @@ def test_an_untouched_task_says_nothing(logged_in_client, mock_api_client):
|
||||
|
||||
|
||||
def test_the_header_offers_completato_not_the_summary(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
html = _execute_page(logged_in_client, mock_api_client)
|
||||
html = _execute_page(logged_in_client, mock_api_client, monkeypatch)
|
||||
assert 'x-data="taskExecute()"' in html
|
||||
# The header control moves the operator on and is gated by the same rule as
|
||||
# the footer, rather than jumping out of the sequence to a recap.
|
||||
@@ -158,19 +164,19 @@ def test_the_header_offers_completato_not_the_summary(
|
||||
|
||||
|
||||
def test_fine_ciclo_is_shown_before_it_can_be_used(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
"""It used to appear only once the task was already finished."""
|
||||
html = _execute_page(logged_in_client, mock_api_client)
|
||||
html = _execute_page(logged_in_client, mock_api_client, monkeypatch)
|
||||
assert 'x-show="!cycleConfirmed"' in html
|
||||
assert ':disabled="!isComplete"' in html
|
||||
assert "Fine ciclo misura" in html
|
||||
|
||||
|
||||
def test_the_missing_quotes_are_counted_out_loud(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
html = _execute_page(logged_in_client, mock_api_client)
|
||||
html = _execute_page(logged_in_client, mock_api_client, monkeypatch)
|
||||
assert "missingQuotesLabel" in html
|
||||
assert "Mancano" in html
|
||||
|
||||
@@ -181,12 +187,12 @@ def test_the_missing_quotes_are_counted_out_loud(
|
||||
|
||||
|
||||
def test_the_operator_flow_never_links_to_the_statistics(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
_operator_only(logged_in_client)
|
||||
pages = [
|
||||
_list_page(logged_in_client, mock_api_client),
|
||||
_execute_page(logged_in_client, mock_api_client),
|
||||
_execute_page(logged_in_client, mock_api_client, monkeypatch),
|
||||
]
|
||||
for html in pages:
|
||||
assert "/statistics" not in html
|
||||
|
||||
@@ -71,7 +71,11 @@ TASK = {
|
||||
}
|
||||
|
||||
|
||||
def _execute_page(client, mock_api_client, allow_manual):
|
||||
def _execute_page(client, mock_api_client, monkeypatch, allow_manual):
|
||||
# task_execute() only renders a recipe assigned to this station (#24).
|
||||
# STATION_CODE defaults to unset in the test env, so it has to be forced.
|
||||
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 3}]
|
||||
mock_api_client.get.side_effect = [
|
||||
TASK,
|
||||
[{"id": 11, "order_index": 0}],
|
||||
@@ -86,9 +90,9 @@ def _execute_page(client, mock_api_client, allow_manual):
|
||||
|
||||
|
||||
def test_keypad_digits_are_absent_when_typing_is_forbidden(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
html = _execute_page(logged_in_client, mock_api_client, allow_manual=False)
|
||||
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, allow_manual=False)
|
||||
assert "allowManual: false" in html
|
||||
assert "addDigit('7')" not in html, "i tasti numerici non devono essere in pagina"
|
||||
# The corrections and the confirm stay: a wrong caliper reading has to be
|
||||
@@ -98,17 +102,19 @@ def test_keypad_digits_are_absent_when_typing_is_forbidden(
|
||||
|
||||
|
||||
def test_keypad_is_there_when_the_recipe_allows_typing(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
html = _execute_page(logged_in_client, mock_api_client, allow_manual=True)
|
||||
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, allow_manual=True)
|
||||
assert "allowManual: true" in html
|
||||
assert "addDigit('7')" in html
|
||||
|
||||
|
||||
def test_keypad_stays_when_the_recipe_cannot_be_read(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
"""A recipe that failed to load must not silently take the keypad away."""
|
||||
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 3}]
|
||||
mock_api_client.get.side_effect = [
|
||||
TASK,
|
||||
[{"id": 11, "order_index": 0}],
|
||||
@@ -125,10 +131,10 @@ def test_keypad_stays_when_the_recipe_cannot_be_read(
|
||||
|
||||
|
||||
def test_the_operator_sees_the_description_formatted(
|
||||
logged_in_client, mock_api_client,
|
||||
logged_in_client, mock_api_client, monkeypatch,
|
||||
):
|
||||
"""It was written for the operator, and until now only the maker saw it."""
|
||||
html = _execute_page(logged_in_client, mock_api_client, allow_manual=True)
|
||||
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, allow_manual=True)
|
||||
assert "prima riga<br><strong>grassetto</strong>" in html
|
||||
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ def mock_measure_api():
|
||||
yield mock
|
||||
|
||||
|
||||
def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api):
|
||||
def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api, monkeypatch):
|
||||
"""The measurement screen carries the most inline JS in the app.
|
||||
|
||||
It drives the numpad, the caliper burst detection, the supervisor modal and -
|
||||
@@ -235,6 +235,9 @@ def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api):
|
||||
here kills every binding on the screen the operator actually works in.
|
||||
"""
|
||||
_force_italian(logged_in_client)
|
||||
# STATION_CODE defaults to unset in the test env, so it has to be forced
|
||||
# for task_execute()'s station-assignment check (#24).
|
||||
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||
task = {
|
||||
"id": 11,
|
||||
"recipe_id": 3,
|
||||
@@ -250,6 +253,8 @@ def test_task_execute_inline_js_is_valid(logged_in_client, mock_measure_api):
|
||||
"unit": "mm",
|
||||
}],
|
||||
}
|
||||
# task_execute() only renders a recipe assigned to this station (#24).
|
||||
mock_measure_api.get_station_recipes.return_value = [{"id": 3}]
|
||||
mock_measure_api.get.side_effect = [
|
||||
task, # /api/tasks/11
|
||||
[{"id": 11, "order_index": 0}], # /api/recipes/3/tasks
|
||||
|
||||
Reference in New Issue
Block a user