"""Tests for cli.py using Click's test runner.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest import responses as responses_lib from click.testing import CliRunner from modpack_checker.cli import cli from modpack_checker.curseforge import CurseForgeNotFoundError @pytest.fixture def runner(): return CliRunner() @pytest.fixture def mock_cfg(tmp_path): """Return a Config-like mock backed by a real temp database.""" cfg = MagicMock() cfg.curseforge_api_key = "test-api-key" cfg.database_path = str(tmp_path / "test.db") cfg.discord_webhook_url = None cfg.notification_on_update = True cfg.check_interval_hours = 6 return cfg # --------------------------------------------------------------------------- # Root / help # --------------------------------------------------------------------------- def test_help(runner): result = runner.invoke(cli, ["--help"]) assert result.exit_code == 0 assert "Modpack Version Checker" in result.output def test_version(runner): result = runner.invoke(cli, ["--version"]) assert result.exit_code == 0 assert "1.0.0" in result.output # --------------------------------------------------------------------------- # config show # --------------------------------------------------------------------------- def test_config_show(runner, mock_cfg): mock_cfg.curseforge_api_key = "abcd1234efgh5678" with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["config", "show"]) assert result.exit_code == 0 assert "abcd" in result.output # first 4 chars assert "5678" in result.output # last 4 chars def test_config_show_no_key(runner, mock_cfg): mock_cfg.curseforge_api_key = "" with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["config", "show"]) assert result.exit_code == 0 assert "Not configured" in result.output # --------------------------------------------------------------------------- # list # --------------------------------------------------------------------------- def test_list_empty(runner, mock_cfg): with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["list"]) assert result.exit_code == 0 assert "empty" in result.output.lower() def test_list_with_modpacks(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(111, "Pack Alpha") db.add_modpack(222, "Pack Beta") mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["list"]) assert result.exit_code == 0 assert "Pack Alpha" in result.output assert "Pack Beta" in result.output # --------------------------------------------------------------------------- # add # --------------------------------------------------------------------------- def test_add_requires_api_key(runner, mock_cfg): mock_cfg.curseforge_api_key = "" with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["add", "12345"]) assert result.exit_code == 1 assert "API key" in result.output @responses_lib.activate def test_add_success(runner, mock_cfg): responses_lib.add( responses_lib.GET, "https://api.curseforge.com/v1/mods/12345", json={"data": {"id": 12345, "name": "Test Modpack", "latestFiles": []}}, status=200, ) with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["add", "12345"]) assert result.exit_code == 0 assert "Test Modpack" in result.output assert "tracking" in result.output.lower() @responses_lib.activate def test_add_not_found(runner, mock_cfg): responses_lib.add( responses_lib.GET, "https://api.curseforge.com/v1/mods/99999", status=404, ) with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["add", "99999"]) assert result.exit_code == 1 assert "99999" in result.output @responses_lib.activate def test_add_duplicate_shows_warning(runner, mock_cfg, tmp_path): responses_lib.add( responses_lib.GET, "https://api.curseforge.com/v1/mods/12345", json={"data": {"id": 12345, "name": "Test Modpack", "latestFiles": []}}, status=200, ) from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Modpack") mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["add", "12345"]) assert "already tracked" in result.output.lower() # --------------------------------------------------------------------------- # remove # --------------------------------------------------------------------------- def test_remove_not_in_list(runner, mock_cfg): with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["remove", "99999"], input="y\n") assert result.exit_code == 1 assert "not in your watch list" in result.output def test_remove_success(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["remove", "12345"], input="y\n") assert result.exit_code == 0 assert "Removed" in result.output def test_remove_aborted(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["remove", "12345"], input="n\n") # Aborted — pack should still exist assert db.get_modpack(12345) is not None # --------------------------------------------------------------------------- # check # --------------------------------------------------------------------------- def test_check_empty_list(runner, mock_cfg): with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["check"]) assert result.exit_code == 0 assert "No modpacks" in result.output @responses_lib.activate def test_check_up_to_date(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") db.update_version(12345, "1.0.0") responses_lib.add( responses_lib.GET, "https://api.curseforge.com/v1/mods/12345", json={ "data": { "id": 12345, "name": "Test Pack", "latestFiles": [ { "id": 1, "displayName": "1.0.0", "fileName": "pack-1.0.0.zip", "fileDate": "2026-01-01T00:00:00Z", } ], } }, status=200, ) mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["check", "--no-notify"]) assert result.exit_code == 0 assert "up to date" in result.output.lower() @responses_lib.activate def test_check_update_available(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") db.update_version(12345, "1.0.0") responses_lib.add( responses_lib.GET, "https://api.curseforge.com/v1/mods/12345", json={ "data": { "id": 12345, "name": "Test Pack", "latestFiles": [ { "id": 2, "displayName": "1.1.0", "fileName": "pack-1.1.0.zip", "fileDate": "2026-02-01T00:00:00Z", } ], } }, status=200, ) mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["check", "--no-notify"]) assert result.exit_code == 0 assert "1.1.0" in result.output # --------------------------------------------------------------------------- # status # --------------------------------------------------------------------------- def test_status_not_found(runner, mock_cfg): with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["status", "99999"]) assert result.exit_code == 1 assert "not in your watch list" in result.output def test_status_shows_info(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") db.update_version(12345, "2.0.0") mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["status", "12345"]) assert result.exit_code == 0 assert "Test Pack" in result.output assert "2.0.0" in result.output # --------------------------------------------------------------------------- # notifications command # --------------------------------------------------------------------------- def test_notifications_disable(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["notifications", "12345", "--disable"]) assert result.exit_code == 0 assert "disabled" in result.output assert db.get_modpack(12345).notification_enabled is False def test_notifications_enable(runner, mock_cfg, tmp_path): from modpack_checker.database import Database db = Database(str(tmp_path / "test.db")) db.add_modpack(12345, "Test Pack") db.toggle_notifications(12345, False) mock_cfg.database_path = str(tmp_path / "test.db") with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["notifications", "12345", "--enable"]) assert result.exit_code == 0 assert "enabled" in result.output assert db.get_modpack(12345).notification_enabled is True def test_notifications_missing_modpack(runner, mock_cfg): with patch("modpack_checker.cli.Config.load", return_value=mock_cfg): result = runner.invoke(cli, ["notifications", "99999", "--enable"]) assert result.exit_code == 1