fix(scripts): guardie di seed_collaudo.py per produzione e cancellazioni (#17)

--supervisor-password non ha piu' un default committato: se il
capoturno va creato e l'opzione manca, ne genera una casuale
(secrets.token_urlsafe(12)) e la stampa una sola volta.

--conferma-produzione richiesto quando APP_ENV (src.backend.config,
stessa regola di #33) non e' dev/test.

--replace elenca quante Measurement/ProductionRun/ProductionEvent
cancellerebbe e si ferma senza cancellare finche' non si aggiunge --si.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:45:14 +02:00
parent 54bc384650
commit 8864b2021b
2 changed files with 280 additions and 11 deletions
+190
View File
@@ -0,0 +1,190 @@
"""Tests for scripts/seed_collaudo.py (issue #17).
The script is not a package module (it is copied into the server container
and run directly), so it is loaded here via importlib from its file path.
"""
import asyncio
import importlib.util
from pathlib import Path
import pytest
from sqlalchemy import select
from src.backend.config import settings
from src.backend.models.orm.station import Station
from src.backend.models.orm.user import User
from src.backend.services.auth_service import create_user
from src.backend.tests.conftest import TestSessionFactory, test_engine
_SCRIPT_PATH = Path(__file__).resolve().parents[3] / "scripts" / "seed_collaudo.py"
def _load_script():
spec = importlib.util.spec_from_file_location("seed_collaudo", _SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.fixture
def seed_collaudo(monkeypatch):
module = _load_script()
monkeypatch.setattr(module, "async_session_factory", TestSessionFactory)
return module
@pytest.fixture(autouse=True)
def reset_app_env(monkeypatch):
"""Every test in this file assumes the default (dev)."""
monkeypatch.setattr(settings, "app_env", "dev")
async def _make_admin_and_station(db_session) -> None:
admin = await create_user(
db_session, username="owner", password="OwnerPass1",
display_name="Owner", is_admin=True,
)
db_session.add(Station(
code="ST-DEFAULT", name="Default", created_by=admin.id,
))
await db_session.flush()
class TestReplaceRequiresSi:
"""--replace without --si only reports the impact, never deletes (#17c)."""
async def test_replace_without_si_does_not_delete(self, seed_collaudo, db_session):
await _make_admin_and_station(db_session)
# First run creates COLLAUDO-A/B.
rc = await seed_collaudo.seed("ST-DEFAULT", False, "FirstPass123", True)
assert rc == 0
# Second run with --replace but no --si: must not touch anything.
rc = await seed_collaudo.seed("ST-DEFAULT", True, "SecondPass123", False)
assert rc == 1
recipes = (await db_session.execute(
select(seed_collaudo.Recipe).where(
seed_collaudo.Recipe.code.in_(seed_collaudo.CODES)
)
)).scalars().all()
assert len(recipes) == 2
async def test_replace_with_si_deletes_and_recreates(self, seed_collaudo, db_session):
await _make_admin_and_station(db_session)
rc = await seed_collaudo.seed("ST-DEFAULT", False, "FirstPass123", True)
assert rc == 0
rc = await seed_collaudo.seed("ST-DEFAULT", True, "SecondPass123", True)
assert rc == 0
recipes = (await db_session.execute(
select(seed_collaudo.Recipe).where(
seed_collaudo.Recipe.code.in_(seed_collaudo.CODES)
)
)).scalars().all()
assert len(recipes) == 2
def test_main_with_fake_argv_replace_without_si_does_not_delete(
self, seed_collaudo, monkeypatch
):
"""The exact scenario from the analysis: main() with a fake argv,
--replace but no --si, must leave the recipes untouched.
Plain `def`, not `async def`: main() manages its own event loop via
asyncio.run(), which cannot be called from inside one already running
(as an `async def` test under pytest-asyncio would have).
"""
async def _setup():
async with TestSessionFactory() as session:
await _make_admin_and_station(session)
await session.commit()
asyncio.run(_setup())
assert asyncio.run(
seed_collaudo.seed("ST-DEFAULT", False, "FirstPass123", True)
) == 0
monkeypatch.setattr(
"sys.argv",
["seed_collaudo.py", "--station", "ST-DEFAULT", "--replace"],
)
rc = seed_collaudo.main()
assert rc == 1
async def _count():
async with TestSessionFactory() as session:
recipes = (await session.execute(
select(seed_collaudo.Recipe).where(
seed_collaudo.Recipe.code.in_(seed_collaudo.CODES)
)
)).scalars().all()
return len(recipes)
assert asyncio.run(_count()) == 2
class TestSupervisorPassword:
"""No default --supervisor-password: generated and printed once (#17a)."""
async def test_generates_random_password_when_not_given(
self, seed_collaudo, db_session
):
await _make_admin_and_station(db_session)
rc = await seed_collaudo.seed("ST-DEFAULT", False, None, True)
assert rc == 0
supervisor = (await db_session.execute(
select(User).where(User.username == "capoturno")
)).scalar_one_or_none()
assert supervisor is not None
# A real password was set (create_user always hashes something).
assert supervisor.password_hash
async def test_uses_given_password_when_provided(self, seed_collaudo, db_session):
await _make_admin_and_station(db_session)
rc = await seed_collaudo.seed("ST-DEFAULT", False, "ExplicitPass1", True)
assert rc == 0
from src.backend.services.auth_service import authenticate_user
user = await authenticate_user(db_session, "capoturno", "ExplicitPass1")
assert user is not None
async def test_existing_supervisor_password_untouched(
self, seed_collaudo, db_session
):
"""A second run (no --replace) must not need a password at all."""
await _make_admin_and_station(db_session)
assert await seed_collaudo.seed("ST-DEFAULT", False, "FirstPass123", True) == 0
# Recipes already exist, so seed() stops at the "already present"
# check before ever looking at supervisor_password.
rc = await seed_collaudo.seed("ST-DEFAULT", False, None, True)
assert rc == 1
class TestProductionGuard:
"""Refuses to run against APP_ENV outside dev/test without --conferma-produzione (#17b)."""
def test_main_refuses_in_production_without_confirmation(
self, seed_collaudo, monkeypatch
):
monkeypatch.setattr(settings, "app_env", "production")
monkeypatch.setattr(
"sys.argv", ["seed_collaudo.py", "--station", "ST-DEFAULT"]
)
rc = seed_collaudo.main()
assert rc == 2
def test_main_allows_dev_without_confirmation(self, seed_collaudo, monkeypatch):
monkeypatch.setattr(settings, "app_env", "dev")
# No admin/station set up: seed() itself will fail with rc=2, but that
# proves the production guard did not block it first.
monkeypatch.setattr(
"sys.argv", ["seed_collaudo.py", "--station", "NOPE"]
)
rc = seed_collaudo.main()
assert rc == 2 # "Nessun utente amministratore" / stazione inesistente