Files
TieMeasureFlow/src/backend/tests/test_station_seed.py
T
Adriano e20299548f fix(migrations): schema base 000 per DB vuoti, stamp head dopo create_all (#6)
upgrade head partiva dalla 001, che sa solo aggiungere colonne a tabelle
che assume gia' esistenti: su un DB davvero vuoto falliva subito. Nuova
000_base_schema.py (revision "000_base", down_revision None) crea lo
schema pre-001 esplicitamente (create_table, non Base.metadata.create_all,
che porterebbe gia' le colonne di 001-013 e farebbe fallire 001 su
"Duplicate column"). No-op se la tabella recipes esiste gia'
(sa.inspect(bind).has_table("recipes")). 001 ora ha down_revision =
"000_base".

setup.py: init-db e reset-db, dopo create_all/drop_all+create_all,
eseguono l'equivalente di "alembic stamp head" (nuovo _stamp_head(),
command.stamp con la config di src/backend/migrations/alembic.ini) cosi'
un DB nato da create_all non ritenta le migrazioni 001-013 al prossimo
deploy.

Test: catena upgrade head su SQLite vuoto, tabelle/colonne confrontate
con Base.metadata; DB con recipes gia' nello stato pre-001 -> upgrade
head non fallisce (000 no-op). _stamp_head si appoggia a
migrations/env.py, che costruisce una connessione propria da
settings.database_url (non modificabile dai test tramite l'engine
patchato di setup.py): nei test di setup.py e' quindi no-op via
monkeypatch, cosi' init-db/reset-db continuano a girare su SQLite senza
toccare un MySQL vero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:49:38 +02:00

110 lines
4.3 KiB
Python

"""Verify /api/setup/seed creates a default station with all recipes assigned.
DEVIATION FROM PLAN: The real setup endpoint is NOT a single /api/setup/initialize.
The setup is split into separate endpoints:
- POST /api/setup/init-db (creates tables)
- POST /api/setup/seed (seeds users + demo recipe + stations)
The seed body is {"password": "..."} with no load_demo_data flag (demo data
is always seeded). The station seed is added inside /api/setup/seed.
The seed endpoint uses async_session_factory directly (not get_db dependency
injection), so we must monkeypatch routers.setup.engine and
routers.setup.async_session_factory to point at the test SQLite engine,
exactly as done in test_setup.py.
"""
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.config import settings
from src.backend.models.orm.station import Station, StationRecipeAssignment
from src.backend.models.orm.recipe import Recipe
from src.backend.tests.conftest import test_engine, TestSessionFactory
SETUP_PWD = "test-setup-pwd"
@pytest.fixture(autouse=True)
def enable_setup(monkeypatch):
"""Enable setup endpoints and redirect engine/session to test SQLite DB."""
monkeypatch.setattr(settings, "setup_password", SETUP_PWD)
import src.backend.api.routers.setup as setup_mod
monkeypatch.setattr(setup_mod, "engine", test_engine)
monkeypatch.setattr(setup_mod, "async_session_factory", TestSessionFactory)
# See test_setup.py: _stamp_head would otherwise try to reach a real
# MySQL server via migrations/env.py.
monkeypatch.setattr(setup_mod, "_stamp_head", lambda: None)
async def _seed(client: AsyncClient) -> dict:
"""Init DB tables then run seed; return seed response JSON."""
resp = await client.post("/api/setup/init-db", json={"password": SETUP_PWD})
assert resp.status_code == 200, resp.text
resp = await client.post("/api/setup/seed", json={"password": SETUP_PWD})
assert resp.status_code == 200, resp.text
return resp.json()
@pytest.mark.asyncio
async def test_setup_seed_creates_default_station_and_assigns_recipes(
client: AsyncClient,
db_session: AsyncSession,
):
"""After seeding, ST-DEFAULT station must exist and all active recipes assigned."""
await _seed(client)
# Default station must exist and be active.
result = await db_session.execute(
select(Station).where(Station.code == "ST-DEFAULT")
)
default = result.scalar_one_or_none()
assert default is not None, "ST-DEFAULT station was not created"
assert default.active is True
# All active recipes must be assigned to the default station.
active_recipes_result = await db_session.execute(
select(Recipe).where(Recipe.active == True)
)
active_recipes = active_recipes_result.scalars().all()
assert len(active_recipes) > 0, "demo seed must create at least one active recipe"
assignments_result = await db_session.execute(
select(StationRecipeAssignment).where(
StationRecipeAssignment.station_id == default.id
)
)
n_assignments = len(assignments_result.scalars().all())
assert n_assignments == len(active_recipes), (
f"Expected {len(active_recipes)} assignment(s), got {n_assignments}"
)
@pytest.mark.asyncio
async def test_setup_seed_station_idempotent(
client: AsyncClient,
db_session: AsyncSession,
monkeypatch,
):
"""Running seed twice must not duplicate ST-DEFAULT or its assignments."""
# First run — creates everything.
await _seed(client)
# Second run: the first seed already created an admin, so this exercises
# setup's own "setup already completed" guard (#9) unless bypassed - the
# idempotency this test is about is seed's, not the guard's.
monkeypatch.setattr(settings, "setup_allow_after_init", True)
# Second run — recipe DEMO-001 already exists → seed returns early.
# ST-DEFAULT must still exist (not re-created, not duplicated).
resp = await client.post("/api/setup/seed", json={"password": SETUP_PWD})
assert resp.status_code == 200, resp.text
stations_result = await db_session.execute(
select(Station).where(Station.code == "ST-DEFAULT")
)
stations = stations_result.scalars().all()
assert len(stations) == 1, f"Expected exactly 1 ST-DEFAULT, got {len(stations)}"