"""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 # --------------------------------------------------------------------------- # _rename_album_dir_inplace (via apply --in-place) # --------------------------------------------------------------------------- class TestRenameAlbumDir: """Tests für automatisches Umbenennen des Album-Verzeichnisses.""" def test_cd_subdir_renames_parent(self, tmp_path: Path) -> None: """input_dir = CD1 → Elternverzeichnis wird umbenannt.""" album_root = tmp_path / "Falscher_Name" cd1 = album_root / "CD1" cd1.mkdir(parents=True) _make_album_json(album_root / "album.json", n_tracks=1) _make_flac(cd1 / "track01.flac") result = runner.invoke(app, [ "apply", str(cd1), str(album_root / "album.json"), "--in-place", ]) assert result.exit_code == 0, result.output expected = tmp_path / "TestAlbum_2024" assert expected.exists(), f"Verzeichnis nicht umbenannt; output: {result.output}" assert not album_root.exists() def test_album_root_as_input_prints_hint(self, tmp_path: Path) -> None: """input_dir = AlbumRoot → kein auto-rename, stattdessen Hinweis.""" album = _make_album_json(tmp_path / "album.json", n_tracks=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 # Verzeichnis nicht umbenannt assert tmp_path.exists() # Aber Hinweis ausgegeben assert "Tipp" in result.output or "mv" in result.output def test_already_correct_name_no_rename(self, tmp_path: Path) -> None: """CD1 als input_dir, Elternverzeichnis heißt bereits korrekt → keine Ausgabe.""" album_root = tmp_path / "TestAlbum_2024" cd1 = album_root / "CD1" cd1.mkdir(parents=True) _make_album_json(album_root / "album.json", n_tracks=1) _make_flac(cd1 / "track01.flac") result = runner.invoke(app, [ "apply", str(cd1), str(album_root / "album.json"), "--in-place", ]) assert result.exit_code == 0, result.output assert album_root.exists() assert "umbenannt" not in result.output def test_target_exists_prints_warning(self, tmp_path: Path) -> None: """Zielverzeichnis existiert bereits → Warnung, kein Umbenennen.""" album_root = tmp_path / "Falscher_Name" cd1 = album_root / "CD1" cd1.mkdir(parents=True) # Ziel existiert bereits (tmp_path / "TestAlbum_2024").mkdir() _make_album_json(album_root / "album.json", n_tracks=1) _make_flac(cd1 / "track01.flac") result = runner.invoke(app, [ "apply", str(cd1), str(album_root / "album.json"), "--in-place", ]) assert result.exit_code == 0, result.output assert album_root.exists() # nicht umbenannt