"""Tests für die CLI-Commands (cli.py) via typer.testing.CliRunner.""" from __future__ import annotations import json import struct from pathlib import Path from unittest.mock import patch from typer.testing import CliRunner from musiksammlung.cli import app from musiksammlung.models import Album, Disc, Track runner = CliRunner() # --------------------------------------------------------------------------- # Hilfsfunktionen (identisch zu test_tagger.py) # --------------------------------------------------------------------------- def _make_flac(path: Path) -> Path: data = b"fLaC" data += b"\x80\x00\x00\x22" data += struct.pack(">HH", 4096, 4096) data += b"\x00\x00\x00\x00\x00\x00" val = (44100 << 44) | (0 << 41) | (15 << 36) | 0 data += val.to_bytes(8, "big") data += b"\x00" * 16 path.write_bytes(data) return path def _make_album_json(path: Path, *, n_tracks: int = 2, n_discs: int = 1) -> Album: discs = [ Disc( disc_number=d, tracks=[ Track(track_number=t, title=f"Disc{d} Track{t}") for t in range(1, n_tracks + 1) ], ) for d in range(1, n_discs + 1) ] album = Album(artist="TestArtist", album="TestAlbum", year=2024, discs=discs) path.write_text(album.model_dump_json(indent=2), encoding="utf-8") return album def _make_disc_files(album_dir: Path, album: Album) -> None: """Erstellt FLAC-Dateien im korrekten Namensschema.""" multi = len(album.discs) > 1 for disc in album.discs: disc_dir = album_dir / f"CD{disc.disc_number}" if multi else album_dir disc_dir.mkdir(parents=True, exist_ok=True) for track in disc.tracks: fname = f"{track.track_number:02d}_-_{track.title.replace(' ', '_')}_-_TestArtist.flac" _make_flac(disc_dir / fname) # --------------------------------------------------------------------------- # apply # --------------------------------------------------------------------------- class TestApplyCommand: def test_apply_inplace_renames_and_tags(self, tmp_path: Path) -> None: _make_album_json(tmp_path / "album.json") # Audiodateien mit track*-Namen (wie nach CDDB-Fehler) _make_flac(tmp_path / "track01.flac") _make_flac(tmp_path / "track02.flac") result = runner.invoke(app, [ "apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place", ]) assert result.exit_code == 0, result.output def test_apply_fails_on_disc_mismatch(self, tmp_path: Path) -> None: _make_album_json(tmp_path / "album.json", n_tracks=3) # Nur 2 Dateien, JSON hat 3 Tracks _make_flac(tmp_path / "track01.flac") _make_flac(tmp_path / "track02.flac") result = runner.invoke(app, [ "apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place", ]) assert result.exit_code == 1 assert "Diskrepanz" in result.output or "Diskrepanz" in (result.stderr or "") def test_apply_dry_run_makes_no_changes(self, tmp_path: Path) -> None: _make_album_json(tmp_path / "album.json") f1 = _make_flac(tmp_path / "track01.flac") f2 = _make_flac(tmp_path / "track02.flac") result = runner.invoke(app, [ "apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place", "--dry-run", ]) assert result.exit_code == 0 assert "DRY-RUN" in result.output # Originaldateien noch vorhanden assert f1.exists() assert f2.exists() def test_apply_missing_json_raises(self, tmp_path: Path) -> None: result = runner.invoke(app, [ "apply", str(tmp_path), str(tmp_path / "nope.json"), "--in-place", ]) assert result.exit_code != 0 def test_apply_creates_playlist(self, tmp_path: Path) -> None: album = _make_album_json(tmp_path / "album.json") _make_disc_files(tmp_path, album) result = runner.invoke(app, [ "apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place", ]) assert result.exit_code == 0, result.output playlists = list(tmp_path.glob("*.m3u")) assert len(playlists) == 1 def test_apply_multi_disc(self, tmp_path: Path) -> None: album = _make_album_json(tmp_path / "album.json", n_tracks=2, n_discs=2) _make_disc_files(tmp_path, album) result = runner.invoke(app, [ "apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place", ]) assert result.exit_code == 0, result.output # --------------------------------------------------------------------------- # check # --------------------------------------------------------------------------- class TestCheckCommand: def test_check_shows_cover_status(self, tmp_path: Path) -> None: (tmp_path / "frontcover.jpg").write_bytes(b"\xff\xd8\xff\xe0") # minimal JPEG magic result = runner.invoke(app, ["check", str(tmp_path)]) assert result.exit_code == 0 assert "frontcover.jpg" in result.output def test_check_shows_missing_cover(self, tmp_path: Path) -> None: result = runner.invoke(app, ["check", str(tmp_path)]) assert result.exit_code == 0 assert "fehlt" in result.output def test_check_lists_audio_files(self, tmp_path: Path) -> None: _make_flac(tmp_path / "01_-_Track_-_Artist.flac") result = runner.invoke(app, ["check", str(tmp_path)]) assert result.exit_code == 0 assert "01_-_Track_-_Artist.flac" in result.output def test_check_nonexistent_directory(self, tmp_path: Path) -> None: result = runner.invoke(app, ["check", str(tmp_path / "nope")]) assert result.exit_code == 1 def test_check_no_audio_files_message(self, tmp_path: Path) -> None: result = runner.invoke(app, ["check", str(tmp_path)]) assert result.exit_code == 0 assert "Keine Audiodateien" in result.output # --------------------------------------------------------------------------- # scan (mockt LLM-Aufruf) # --------------------------------------------------------------------------- class TestScanCommand: def test_scan_from_text_creates_json(self, tmp_path: Path) -> None: text_file = tmp_path / "tracklist.txt" text_file.write_text("1. Allegro\n2. Andante", encoding="utf-8") output = tmp_path / "album.json" fake_album = Album( artist="Karajan", album="Beethoven", year=1963, discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="Allegro")])], ) with patch("musiksammlung.cli.parse_tracklist", return_value=fake_album): result = runner.invoke(app, [ "scan", "--from-text", str(text_file), "--output", str(output), ]) assert result.exit_code == 0, result.output assert output.exists() data = json.loads(output.read_text()) assert data["artist"] == "Karajan" def test_scan_missing_text_file(self, tmp_path: Path) -> None: result = runner.invoke(app, [ "scan", "--from-text", str(tmp_path / "nope.txt"), ]) assert result.exit_code == 1 def test_scan_no_args_fails(self) -> None: result = runner.invoke(app, ["scan"]) assert result.exit_code == 1