Files
TieMeasureFlow/src/backend/tests/test_station_seed.py
T
Adriano 1a0431366f chore(v2): restructure monorepo to src/ layout with uv
Aligns the repo with the python-project-spec-design.md template chosen
for V2.0.0. Big move, no logic changes. The 3 pre-existing test
failures (test_recipes::test_update_recipe, test_recipes::
test_recipe_versioning, test_tasks::test_reorder_tasks, plus the
client test_save_measurement_proxy) survive unchanged.

Layout changes
- server/        -> src/backend/
- server/middleware/ -> src/backend/api/middleware/
- server/routers/    -> src/backend/api/routers/
- server/models/     -> src/backend/models/orm/
- server/schemas/    -> src/backend/models/api/
- server/uploads/    -> uploads/ (project root, mounted volume)
- server/tests/      -> src/backend/tests/
- client/            -> src/frontend/flask_app/ (Flask kept; React
  deroga is documented in CLAUDE.md, justified by tablet UX, USB
  caliper/barcode workflow and Fabric.js integration)

Tooling
- pyproject.toml: monorepo with [project] core deps and
  optional-dependencies server / client / dev. Replaces both
  server/requirements.txt and client/requirements.txt.
- uv.lock + .python-version (3.11) committed for reproducible builds.
- Dockerfile (root, backend) and Dockerfile.frontend rewritten to use
  uv sync --frozen --no-dev --extra server|client; legacy Dockerfiles
  preserved as Dockerfile.legacy for reference but excluded from build
  context via .dockerignore.
- docker-compose.dev.yml + docker-compose.yml: build context now ".",
  dockerfile pointing to the root files.

Code adjustments forced by the move
- Every "from config|database|models|schemas|services|routers|middleware
  import ..." rewritten to its src.backend.* equivalent (50+ files
  including indented inline imports inside test bodies).
- src/backend/migrations/env.py: insert project root into sys.path so
  alembic can resolve src.backend.* imports regardless of cwd.
- src/backend/config.py: env_file ../../.env (was ../.env), upload_path
  resolves project root via parents[2].
- src/backend/tests/conftest.py + tests: import ... from src.backend.*
  instead of bare names; old per-directory pytest.ini files removed in
  favor of root pyproject.toml [tool.pytest.ini_options].
- .gitignore: uploads/ at root, src/frontend/flask_app/static/css/
  tailwind.css path; .dockerignore tightened.
- CLAUDE.md: rewrote sections "Layout del repository", "Comandi di
  Sviluppo", "Database & Migrations", "Test", "i18n", and all path
  references throughout the architecture sections.

Verified
- uv lock resolves 77 packages; uv sync --extra server --extra client
  --extra dev installs cleanly.
- uv run pytest: 171 passed, 4 pre-existing failures.
- uv run alembic -c src/backend/migrations/alembic.ini check loads
  config and metadata (errors only on the absent local MySQL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:26:47 +02:00

101 lines
3.8 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)
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,
):
"""Running seed twice must not duplicate ST-DEFAULT or its assignments."""
# First run — creates everything.
await _seed(client)
# 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)}"