"""Backend su un file SQLite locale (pacchetto Windows, TieMeasureFlow#77). Il modulo `database` costruisce l'engine all'import leggendo l'ambiente, quindi i test che riguardano DB_URL girano in un sottoprocesso con DB_URL/UPLOAD_DIR impostati: e' lo stesso percorso di avvio.py nel pacchetto, senza toccare l'engine in memoria usato dal resto della suite. """ import json import os import subprocess import sys import textwrap from pathlib import Path from src.backend.config import Settings _ROOT = Path(__file__).resolve().parents[3] # Script eseguito nel sottoprocesso: upgrade head da DB vuoto, poi app reale # (health, login con utente creato via ORM), PRAGMA e vincolo FK. _SCRIPT = textwrap.dedent( """ import asyncio, json, sys from pathlib import Path from unittest.mock import MagicMock # WeasyPrint senza GTK: stesso mock di conftest.py (le DLL sono affare del piano B) sys.modules.setdefault("weasyprint", MagicMock()) from alembic import command from alembic.config import Config from src.backend.config import settings ini = Path(r"{ini}") command.upgrade(Config(str(ini)), "head") from httpx import ASGITransport, AsyncClient from sqlalchemy import select, text from sqlalchemy.exc import IntegrityError from src.backend.database import async_session_factory, engine from src.backend.main import app from src.backend.models.orm.user import User from src.backend.models.orm.recipe import RecipeVersion from src.backend.services.auth_service import hash_password async def main(): out = {{}} async with async_session_factory() as s: s.add(User(username="fiera", password_hash=hash_password("Pass12345"), display_name="Fiera", roles=["Maker"], is_admin=True)) await s.commit() async with async_session_factory() as s: out["utente"] = (await s.execute(select(User.username))).scalars().all() out["fk"] = (await s.execute(text("PRAGMA foreign_keys"))).scalar() out["journal"] = (await s.execute(text("PRAGMA journal_mode"))).scalar() out["busy"] = (await s.execute(text("PRAGMA busy_timeout"))).scalar() # FK attive: una versione con recipe_id inesistente deve essere rifiutata try: async with async_session_factory() as s: s.add(RecipeVersion(recipe_id=99999, version_number=1, created_by=1)) await s.commit() out["fk_violation"] = False except IntegrityError: out["fk_violation"] = True async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c: out["health"] = (await c.get("/api/health")).status_code r = await c.post("/api/auth/login", json={{"username": "fiera", "password": "Pass12345"}}) out["login"] = r.status_code out["upload_path"] = str(settings.upload_path) out["database_url"] = settings.database_url await engine.dispose() print("RISULTATO=" + json.dumps(out)) asyncio.run(main()) """ ) def _run(tmp_path: Path, extra_env: dict[str, str]) -> dict: ini = _ROOT / "src" / "backend" / "migrations" / "alembic.ini" env = {k: v for k, v in os.environ.items() if k not in ("DB_URL", "UPLOAD_DIR")} env.update(extra_env) env["PYTHONPATH"] = str(_ROOT) env["PYTHONIOENCODING"] = "utf-8" proc = subprocess.run( [sys.executable, "-c", _SCRIPT.format(ini=str(ini))], cwd=str(_ROOT), env=env, capture_output=True, text=True, timeout=180, ) assert proc.returncode == 0, proc.stderr[-3000:] line = [l for l in proc.stdout.splitlines() if l.startswith("RISULTATO=")][-1] return json.loads(line[len("RISULTATO="):]) def test_file_sqlite_upgrade_app_pragma_e_upload_assoluto(tmp_path): db = tmp_path / "tmf.db" uploads = tmp_path / "uploads" # barre "/" nell'URL come nel pacchetto Windows (D:/... non va in escape) url = f"sqlite+aiosqlite:///{db.as_posix()}" out = _run(tmp_path, {"DB_URL": url, "UPLOAD_DIR": str(uploads)}) assert out["database_url"] == url assert db.exists() # il file e' stato creato da upgrade head assert out["utente"] == ["fiera"] assert out["health"] == 200 assert out["login"] == 200 assert out["fk"] == 1 # PRAGMA foreign_keys attivo assert out["fk_violation"] is True assert out["journal"] == "wal" assert out["busy"] == 5000 # UPLOAD_DIR assoluto rispettato (non accodato alla radice del progetto) assert Path(out["upload_path"]) == uploads def test_database_url_senza_db_url_resta_mysql(monkeypatch): monkeypatch.delenv("DB_URL", raising=False) s = Settings(_env_file=None, db_url=None) assert s.database_url.startswith("mysql+asyncmy://") def test_db_url_vince_sulle_parti_db(monkeypatch): s = Settings(_env_file=None, db_url="sqlite+aiosqlite:///C:/x/tmf.db", db_host="altro") assert s.database_url == "sqlite+aiosqlite:///C:/x/tmf.db" def test_produzione_con_sqlite_non_richiede_db_password(): # niente RuntimeError: con SQLite la password DB di default non conta Settings( _env_file=None, app_env="production", server_secret_key="x" * 40, db_url="sqlite+aiosqlite:///C:/x/tmf.db", ) def test_produzione_mysql_rifiuta_ancora_password_di_default(): import pytest with pytest.raises(Exception, match="DB_PASSWORD"): Settings(_env_file=None, app_env="production", server_secret_key="x" * 40) def test_upload_dir_relativo_resta_sotto_la_radice(): s = Settings(_env_file=None, upload_dir="uploads") assert s.upload_path == _ROOT / "uploads" def test_upload_dir_assoluto(tmp_path): s = Settings(_env_file=None, upload_dir=str(tmp_path / "u")) assert s.upload_path == tmp_path / "u"