Files
alembic/tests/test_playlist_service.py
andrew e49d12a72b Add test suite (T1)
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>
2026-07-10 16:00:07 -06:00

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")