e49d12a72b
First automated coverage. 38 tests, run against the built image (which has the app deps); tests/ and requirements-dev.txt are not baked into the runtime image. - tests/test_dedup_gating.py: the safety-critical one. Drives dedup-library.sh's process_group with --apply + --only-paths and asserts ONLY the confirmed path is deleted and the higher-ranked FLAC is kept. - tests/test_pipeline_runner.py: lock skip, use_lock=False bypass, lock release, success/failure/timeout recording. - tests/test_db.py: schema version + idempotent migrations, FK-safe job-run pruning, stale-run reconciliation. - tests/test_credential_service.py: conf-field substitution incl. newline- injection safety, secret/non-secret classification, cookie-expiry parsing. - tests/test_playlist_service.py: cron round-trips, name validation, render. - tests/test_diagnostics.py: /health + /setup checks. - conftest.py points every path setting at throwaway temp dirs and a temp DB, so tests never touch a real library, /config, or the network. See tests/README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
1.9 KiB
Python
68 lines
1.9 KiB
Python
"""Test setup.
|
|
|
|
Every path-derived setting must point at throwaway dirs BEFORE any app module
|
|
imports, because app.db builds its SQLAlchemy engine from settings at import
|
|
time. So this runs at module load (conftest is imported before test modules).
|
|
"""
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from cryptography.fernet import Fernet
|
|
|
|
_REPO = Path(__file__).resolve().parent.parent
|
|
_TMP = Path(tempfile.mkdtemp(prefix="alembic-tests-"))
|
|
(_TMP / "config" / "pipeline").mkdir(parents=True, exist_ok=True)
|
|
(_TMP / "music").mkdir(parents=True, exist_ok=True)
|
|
|
|
_KEY = _TMP / "master.key"
|
|
_KEY.write_bytes(Fernet.generate_key())
|
|
|
|
os.environ["ALEMBIC_CONFIG_DIR"] = str(_TMP / "config")
|
|
os.environ["MUSIC_DATA_DIR"] = str(_TMP / "music")
|
|
os.environ["PIPELINE_DIR"] = str(_REPO / "pipeline")
|
|
os.environ["ENCRYPTION_KEY_FILE"] = str(_KEY)
|
|
os.environ["SESSION_SECRET"] = "test-only-secret"
|
|
# Make sure no stray OIDC env leaks in from the host shell.
|
|
for _v in ("POCKETID_ISSUER", "POCKETID_CLIENT_ID", "POCKETID_CLIENT_SECRET", "ALLOWED_EMAIL"):
|
|
os.environ.pop(_v, None)
|
|
|
|
import pytest # noqa: E402
|
|
|
|
_TABLES = [
|
|
"job_runs", "dedup_candidates", "dedup_runs", "genre_candidates", "genre_runs",
|
|
"manual_imports", "manual_fix_audit", "playlists", "secrets", "scheduled_jobs",
|
|
"users", "app_settings",
|
|
]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def fresh_db():
|
|
"""Each test gets an initialized, empty database (the engine is bound to the
|
|
one temp DB path, so clear rows between tests rather than rebinding)."""
|
|
from sqlalchemy import text
|
|
|
|
from app.db import engine, init_db
|
|
|
|
init_db()
|
|
with engine.begin() as conn:
|
|
for table in _TABLES:
|
|
conn.execute(text(f"DELETE FROM {table}"))
|
|
yield
|
|
|
|
|
|
@pytest.fixture()
|
|
def db():
|
|
from app.db import SessionLocal
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
yield session
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@pytest.fixture()
|
|
def config_dir():
|
|
return _TMP / "config"
|