fix(production): ruolo e assegnazione stazione su apertura/avanzamento run (#24 backend)
api/routers/production.py: open/cycle/remeasure/pause/resume/close accettavano
qualunque utente autenticato (get_current_user). Nuova dipendenza locale
_require_run_operator (MeasurementTec o Supervisor o admin) sui sei endpoint
di ciclo vita; le letture (current, {run_id} GET) restano aperte a tutti gli
autenticati. Per pause/resume/close si aggiunge al controllo esistente delle
credenziali del supervisore in corpo, che riguarda un'altra persona (chi
autorizza), non chi opera.
services/production_service.py open_run: nessun controllo su recipe.active ne'
sull'assegnazione ricetta-stazione. Nuovo _require_recipe_assigned_to_station:
se la stazione ha almeno un'assegnazione (StationRecipeAssignment), la ricetta
deve esserci, altrimenti 409; una stazione senza alcuna assegnazione resta
libera - stessa regola del filtro in lettura di station_service.list_station_recipes
(join che senza righe non filtra nulla). Ricetta non active -> 409.
NON TOCCATO: src/frontend/flask_app/blueprints/measure.py (stesso filtro sul
lato Flask, lotto B2) e test_measure_station_filter.py - altro agente ci
lavora in parallelo sulla stessa cartella.
Test: src/backend/tests/test_production_runs.py, 6 nuovi test (ruolo su
open/pause, assegnazione rifiutata/accettata/libera, ricetta inattiva).
PROVA: uv run --extra server --extra dev pytest src/backend/tests/test_production_runs.py -q -> 34 passed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,97 @@ async def test_open_run_rejects_unknown_station(
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Role and station-assignment gate (gitea#24)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_open_run_requires_measurement_tec_or_supervisor(
|
||||
client: AsyncClient, maker_user, admin_user, db_session,
|
||||
):
|
||||
"""A Maker with no floor role must not be able to start a production."""
|
||||
station = await _station(db_session, admin_user.id, code="ST-ROLE")
|
||||
recipe = await _recipe_with_interval(db_session, admin_user.id, 15, code="REC-ROLE")
|
||||
|
||||
resp = await _open(client, maker_user, station, recipe)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
async def test_open_run_rejects_recipe_not_assigned_to_station(
|
||||
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||
):
|
||||
from src.backend.services import station_service
|
||||
|
||||
station = await _station(db_session, admin_user.id, code="ST-ASSIGN")
|
||||
assigned_recipe = await _recipe_with_interval(
|
||||
db_session, admin_user.id, 15, code="REC-ASSIGNED"
|
||||
)
|
||||
other_recipe = await _recipe_with_interval(
|
||||
db_session, admin_user.id, 15, code="REC-OTHER"
|
||||
)
|
||||
await station_service.assign_recipe(db_session, station.id, assigned_recipe.id, admin_user)
|
||||
await db_session.commit()
|
||||
|
||||
resp = await _open(client, measurement_tec_user, station, other_recipe)
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
async def test_open_run_allows_recipe_assigned_to_station(
|
||||
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||
):
|
||||
from src.backend.services import station_service
|
||||
|
||||
station = await _station(db_session, admin_user.id, code="ST-ASSIGNOK")
|
||||
recipe = await _recipe_with_interval(db_session, admin_user.id, 15, code="REC-ASSIGNOK")
|
||||
await station_service.assign_recipe(db_session, station.id, recipe.id, admin_user)
|
||||
await db_session.commit()
|
||||
|
||||
resp = await _open(client, measurement_tec_user, station, recipe)
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
async def test_open_run_allows_any_recipe_when_station_has_no_assignment(
|
||||
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||
):
|
||||
"""An unconfigured station is not restricted - matches the read-side filter
|
||||
in station_service.list_station_recipes (empty assignments = not filtering)."""
|
||||
station = await _station(db_session, admin_user.id, code="ST-FREE")
|
||||
recipe = await _recipe_with_interval(db_session, admin_user.id, 15, code="REC-FREE")
|
||||
|
||||
resp = await _open(client, measurement_tec_user, station, recipe)
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
async def test_open_run_rejects_inactive_recipe(
|
||||
client: AsyncClient, measurement_tec_user, admin_user, db_session,
|
||||
):
|
||||
station = await _station(db_session, admin_user.id, code="ST-INACTIVE")
|
||||
recipe = await _recipe_with_interval(db_session, admin_user.id, 15, code="REC-INACTIVE")
|
||||
recipe.active = False
|
||||
await db_session.commit()
|
||||
|
||||
resp = await _open(client, measurement_tec_user, station, recipe)
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
async def test_pause_requires_measurement_tec_or_supervisor_actor(
|
||||
client: AsyncClient, maker_user, measurement_tec_user, admin_user, db_session,
|
||||
):
|
||||
"""The role gate applies to the acting user - separate from the supervisor
|
||||
credentials pause already requires in its own body."""
|
||||
station = await _station(db_session, admin_user.id, code="ST-ROLEPAUSE")
|
||||
recipe = await _recipe_with_interval(db_session, admin_user.id, 10, code="REC-ROLEPAUSE")
|
||||
run_id = (await _open(client, measurement_tec_user, station, recipe)).json()["id"]
|
||||
supervisor, password = await _supervisor(db_session, username="capo-rolepause")
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/production-runs/{run_id}/pause",
|
||||
headers=auth_headers(maker_user),
|
||||
json={"supervisor_username": supervisor.username, "supervisor_password": password},
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reading the current run - what replaces the in-page state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user