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>
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
import pytest
|
|
|
|
from app.services import playlist_service as pl
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"time_str,cron",
|
|
[("01:30", "30 1 * * *"), ("00:00", "0 0 * * *"), ("23:59", "59 23 * * *")],
|
|
)
|
|
def test_cron_roundtrip(time_str, cron):
|
|
assert pl.time_to_cron(time_str) == cron
|
|
assert pl.cron_to_time(cron) == time_str
|
|
|
|
|
|
def test_time_to_cron_invalid():
|
|
assert pl.time_to_cron("") is None
|
|
assert pl.time_to_cron("25:00") is None
|
|
assert pl.time_to_cron("nonsense") is None
|
|
|
|
|
|
def test_cron_to_time_non_daily_is_blank():
|
|
# weekly/day-of-month crons aren't representable as a plain HH:MM
|
|
assert pl.cron_to_time("30 1 * * sun") == ""
|
|
assert pl.cron_to_time(None) == ""
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["techno", "my chill mix", "goldenera", "a-b_c 1"])
|
|
def test_validate_name_accepts(name):
|
|
assert pl.validate_name(name) == name.strip()
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["", "../escape", "a/b", "x;rm -rf", "x|e", "ü", "x" * 65])
|
|
def test_validate_name_rejects(name):
|
|
with pytest.raises(ValueError):
|
|
pl.validate_name(name)
|
|
|
|
|
|
def test_create_renders_conf_and_delete_removes_it(db, config_dir):
|
|
p = pl.create(db, name="unittest", spotify_url="https://open.spotify.com/playlist/ABC")
|
|
conf = config_dir / "pipeline" / "unittest.conf"
|
|
assert conf.exists()
|
|
text = conf.read_text()
|
|
assert "input = https://open.spotify.com/playlist/ABC" in text
|
|
assert "path = " in text and "/unittest" in text
|
|
pl.delete(db, p.id)
|
|
assert not conf.exists()
|
|
|
|
|
|
def test_create_rejects_bad_name(db):
|
|
with pytest.raises(ValueError):
|
|
pl.create(db, name="../evil", spotify_url="https://open.spotify.com/playlist/X")
|