feat(measure): proxy JSON per il progresso di un task (#37.8, parziale)

GET /measure/api/task-progress/<task_id>: il browser non puo' chiamare
/api/measurements/task-progress del backend (niente X-API-Key). Login +
stesso controllo di stazione di task_execute() (_recipe_allowed_at_station),
poi inoltra a _task_progress() con lo stesso version_id/production_run_id
gia' usato da task_list.html.

Non tocca task_execute.html: /api/measurements/task-progress risponde
con conteggi aggregati per task (quotes/measured/state), non con le
quote-per-quota misurate (subtask_id, value, pass_fail, deviation) che
`this.measurements` richiede - ripristinarlo avrebbe richiesto inventare
valori o uno stato non offerto dal backend. MANCA: mostrato in dettaglio
nel rapporto finale.

Test Flask della route: riga per il task richiesto, default a "empty"
se assente, 403 fuori stazione, propagazione 404, login richiesto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 23:36:42 +02:00
parent b27d56d4f7
commit cc686129c0
2 changed files with 113 additions and 0 deletions
@@ -167,6 +167,87 @@ class TestTaskList:
assert "Misura" in resp.get_data(as_text=True)
class TestApiTaskProgress:
"""GET /measure/api/task-progress/<task_id> tests (gitea#37.8).
The browser cannot call /api/measurements/task-progress directly (no
X-API-Key); this proxy forwards it with the same version_id/
production_run_id scoping _task_progress() already uses for task_list.
"""
@pytest.fixture(autouse=True)
def _station_configured(self, monkeypatch):
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
def test_returns_this_task_progress_row(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/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
"/api/production-runs/current": None,
"/api/measurements/task-progress": {
"tasks": [{"task_id": 7, "quotes": 3, "measured": 1, "state": "partial"}],
},
})
resp = logged_in_client.get("/measure/api/task-progress/7")
assert resp.status_code == 200
assert resp.get_json() == {
"task_id": 7, "quotes": 3, "measured": 1, "state": "partial",
}
# No production run open: the backend call must not carry a
# production_run_id it would then use to scope by the wrong cycle.
progress_call = next(
call for call in mock_api_client.get.call_args_list
if call.args[0] == "/api/measurements/task-progress"
)
assert progress_call.kwargs["params"] == {"version_id": 42}
def test_task_absent_from_progress_defaults_to_empty(
self, logged_in_client, mock_api_client,
):
"""A task with no quotes yet has no row in the backend's answer."""
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
mock_api_client.get.side_effect = api_get_router({
"/api/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
"/api/production-runs/current": None,
"/api/measurements/task-progress": {"tasks": []},
})
resp = logged_in_client.get("/measure/api/task-progress/7")
assert resp.status_code == 200
assert resp.get_json() == {
"task_id": 7, "quotes": 0, "measured": 0, "state": "empty",
}
def test_refuses_a_recipe_not_assigned_to_this_station(
self, logged_in_client, mock_api_client,
):
"""Same #24 guard task_execute() uses: no bypass through this proxy."""
mock_api_client.get_station_recipes.return_value = [{"id": 99}]
mock_api_client.get.side_effect = api_get_router({
"/api/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
})
resp = logged_in_client.get("/measure/api/task-progress/7")
assert resp.status_code == 403
def test_propagates_a_task_not_found(self, logged_in_client, mock_api_client):
mock_api_client.get.side_effect = api_get_router({
"/api/tasks/999": {"error": True, "status_code": 404, "detail": "not found"},
})
resp = logged_in_client.get("/measure/api/task-progress/999")
assert resp.status_code == 404
def test_requires_login(self, client):
resp = client.get("/measure/api/task-progress/7")
assert resp.status_code in (302, 401)
class TestStartSequence:
"""GET /measure/start/<recipe_id> — the operator's way in."""