diff --git a/tests/test_cli.py b/tests/test_cli.py index fd41bc1..726c944 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,91 +6,224 @@ import pytest import skywipe.cli as cli +TEST_COMMAND = "posts" +TEST_ERROR_MESSAGE = "boom" +TEST_LOGGER_NAME = "test.cli" + + +def _setup_parser_mocks(monkeypatch, commands=None): + if commands is None: + commands = {TEST_COMMAND: "only posts"} + monkeypatch.setattr(cli.registry, "get_all_commands", lambda: commands) + + +def _setup_main_mocks(monkeypatch, calls, requires_config=False, config_data=None): + _setup_parser_mocks(monkeypatch) + monkeypatch.setattr(cli.registry, "requires_config", + lambda name: requires_config) + + def mock_execute(name, skip_confirmation=False): + calls["execute"] = (name, skip_confirmation) + + def mock_setup_logger(verbose, log_file): + calls["setup"].append((verbose, log_file)) + + def mock_require_config(logger): + calls["require_config"].append(logger) + + monkeypatch.setattr(cli.registry, "execute", mock_execute) + monkeypatch.setattr(cli, "setup_logger", mock_setup_logger) + monkeypatch.setattr(cli, "require_config", mock_require_config) + monkeypatch.setattr(cli, "get_logger", + lambda: logging.getLogger(TEST_LOGGER_NAME)) + + if config_data is not None: + monkeypatch.setattr(cli.Configuration, "load", + lambda self: config_data) + + +def _setup_error_mocks(monkeypatch, calls, error_factory): + _setup_parser_mocks(monkeypatch) + monkeypatch.setattr(cli.registry, "requires_config", lambda name: False) + monkeypatch.setattr(cli.registry, "execute", error_factory) + monkeypatch.setattr(cli, "setup_logger", lambda verbose, log_file: None) + monkeypatch.setattr(cli, "get_logger", + lambda: logging.getLogger(TEST_LOGGER_NAME)) + + def mock_handle_error(error, logger, exit_on_error=False): + calls["handle_error"] = (type(error).__name__, + str(error), exit_on_error) + + monkeypatch.setattr(cli, "handle_error", mock_handle_error) + + def test_create_parser_includes_commands(monkeypatch): - monkeypatch.setattr(cli.registry, "get_all_commands", - lambda: {"posts": "only posts"}) + _setup_parser_mocks(monkeypatch) parser = cli.create_parser() - args = parser.parse_args(["posts"]) - assert args.command == "posts" + args = parser.parse_args([TEST_COMMAND]) + assert args.command == TEST_COMMAND + + +def test_create_parser_handles_multiple_commands(monkeypatch): + commands = { + "posts": "only posts", + "likes": "only likes", + "reposts": "only reposts" + } + _setup_parser_mocks(monkeypatch, commands) + parser = cli.create_parser() + + args1 = parser.parse_args(["posts"]) + args2 = parser.parse_args(["likes"]) + args3 = parser.parse_args(["reposts"]) + + assert args1.command == "posts" + assert args2.command == "likes" + assert args3.command == "reposts" + + +def test_create_parser_parses_yes_flag(monkeypatch): + _setup_parser_mocks(monkeypatch) + parser = cli.create_parser() + args = parser.parse_args(["--yes", TEST_COMMAND]) + assert args.command == TEST_COMMAND + assert args.yes is True + + +def test_create_parser_parses_without_yes_flag(monkeypatch): + _setup_parser_mocks(monkeypatch) + parser = cli.create_parser() + args = parser.parse_args([TEST_COMMAND]) + assert args.command == TEST_COMMAND + assert getattr(args, "yes", False) is False + + +def test_create_parser_version_flag_exits(monkeypatch): + _setup_parser_mocks(monkeypatch) + parser = cli.create_parser() + with pytest.raises(SystemExit) as excinfo: + parser.parse_args(["--version"]) + assert excinfo.value.code == 0 + + +def test_create_parser_requires_command(monkeypatch): + _setup_parser_mocks(monkeypatch) + parser = cli.create_parser() + with pytest.raises(SystemExit): + parser.parse_args([]) + + +def test_create_parser_rejects_invalid_command(monkeypatch): + _setup_parser_mocks(monkeypatch) + parser = cli.create_parser() + with pytest.raises(SystemExit): + parser.parse_args(["invalid_command"]) def test_require_config_exits_when_missing(monkeypatch): monkeypatch.setattr(cli.Configuration, "exists", lambda self: False) - logger = logging.getLogger("test.cli") + logger = logging.getLogger(TEST_LOGGER_NAME) with pytest.raises(SystemExit) as excinfo: cli.require_config(logger) assert excinfo.value.code == 1 +def test_require_config_does_not_exit_when_exists(monkeypatch): + monkeypatch.setattr(cli.Configuration, "exists", lambda self: True) + logger = logging.getLogger(TEST_LOGGER_NAME) + cli.require_config(logger) + + def test_main_executes_without_config(monkeypatch): - calls = {"execute": None, "setup": []} + calls = {"execute": None, "setup": [], "require_config": []} + _setup_main_mocks(monkeypatch, calls, requires_config=False) - monkeypatch.setattr(cli.registry, "get_all_commands", - lambda: {"posts": "only posts"}) - monkeypatch.setattr(cli.registry, "requires_config", lambda name: False) - monkeypatch.setattr(cli.registry, "execute", lambda name, skip_confirmation=False: calls.update( - {"execute": (name, skip_confirmation)} - )) - monkeypatch.setattr(cli, "setup_logger", lambda verbose, - log_file: calls["setup"].append((verbose, log_file))) - monkeypatch.setattr(cli, "get_logger", - lambda: logging.getLogger("test.cli")) - - monkeypatch.setattr(sys, "argv", ["skywipe", "--yes", "posts"]) + monkeypatch.setattr(sys, "argv", ["skywipe", "--yes", TEST_COMMAND]) cli.main() + assert len(calls["require_config"]) == 0 assert calls["setup"] == [(False, cli.LOG_FILE)] - assert calls["execute"] == ("posts", True) + assert calls["execute"] == (TEST_COMMAND, True) def test_main_loads_config_and_sets_verbose(monkeypatch): - calls = {"setup": [], "execute": None, "require_config": 0} + calls = {"setup": [], "execute": None, "require_config": []} + _setup_main_mocks(monkeypatch, calls, requires_config=True, + config_data={"verbose": True}) - monkeypatch.setattr(cli.registry, "get_all_commands", - lambda: {"posts": "only posts"}) - monkeypatch.setattr(cli.registry, "requires_config", lambda name: True) - monkeypatch.setattr(cli.registry, "execute", lambda name, skip_confirmation=False: calls.update( - {"execute": (name, skip_confirmation)} - )) - monkeypatch.setattr(cli, "require_config", lambda logger: calls.update( - {"require_config": calls["require_config"] + 1} - )) - monkeypatch.setattr(cli.Configuration, "load", - lambda self: {"verbose": True}) - monkeypatch.setattr(cli, "setup_logger", lambda verbose, - log_file: calls["setup"].append((verbose, log_file))) - monkeypatch.setattr(cli, "get_logger", - lambda: logging.getLogger("test.cli")) - - monkeypatch.setattr(sys, "argv", ["skywipe", "posts"]) + monkeypatch.setattr(sys, "argv", ["skywipe", TEST_COMMAND]) cli.main() - assert calls["require_config"] == 1 + assert len(calls["require_config"]) == 1 assert calls["setup"] == [(False, cli.LOG_FILE), (True, cli.LOG_FILE)] - assert calls["execute"] == ("posts", False) + assert calls["execute"] == (TEST_COMMAND, False) -def test_main_handles_execute_error(monkeypatch): - calls = {"handle_error": None} +@pytest.mark.parametrize("config_data,expected_verbose", [ + ({}, False), + ({"verbose": False}, False), +]) +def test_main_config_verbose_defaults(monkeypatch, config_data, expected_verbose): + calls = {"setup": [], "execute": None, "require_config": []} + _setup_main_mocks(monkeypatch, calls, requires_config=True, + config_data=config_data) - monkeypatch.setattr(cli.registry, "get_all_commands", - lambda: {"posts": "only posts"}) - monkeypatch.setattr(cli.registry, "requires_config", lambda name: False) + monkeypatch.setattr(sys, "argv", ["skywipe", TEST_COMMAND]) + cli.main() - def raise_error(*_args, **_kwargs): - raise ValueError("boom") + assert len(calls["require_config"]) == 1 + assert calls["setup"] == [(False, cli.LOG_FILE), + (expected_verbose, cli.LOG_FILE)] + assert calls["execute"] == (TEST_COMMAND, False) - monkeypatch.setattr(cli.registry, "execute", raise_error) + +def test_main_handles_config_load_error(monkeypatch): + calls = {"handle_error": None, "require_config": []} + + def mock_require_config(logger): + calls["require_config"].append(logger) + + def raise_config_error(self): + raise RuntimeError("config error") + + def mock_handle_error(error, logger, exit_on_error=False): + calls["handle_error"] = (type(error).__name__, + str(error), exit_on_error) + + _setup_parser_mocks(monkeypatch) + monkeypatch.setattr(cli.registry, "requires_config", lambda name: True) + monkeypatch.setattr(cli, "require_config", mock_require_config) + monkeypatch.setattr(cli.Configuration, "load", raise_config_error) monkeypatch.setattr(cli, "setup_logger", lambda verbose, log_file: None) monkeypatch.setattr(cli, "get_logger", - lambda: logging.getLogger("test.cli")) - - def fake_handle_error(error, logger, exit_on_error=False): - calls["handle_error"] = (str(error), exit_on_error) - - monkeypatch.setattr(cli, "handle_error", fake_handle_error) - monkeypatch.setattr(sys, "argv", ["skywipe", "posts"]) + lambda: logging.getLogger(TEST_LOGGER_NAME)) + monkeypatch.setattr(cli, "handle_error", mock_handle_error) + monkeypatch.setattr(sys, "argv", ["skywipe", TEST_COMMAND]) cli.main() - assert calls["handle_error"] == ("boom", True) + assert len(calls["require_config"]) == 1 + assert calls["handle_error"] is not None + assert calls["handle_error"][0] == "RuntimeError" + assert calls["handle_error"][2] is True + + +@pytest.mark.parametrize("error_class,error_message", [ + (ValueError, TEST_ERROR_MESSAGE), + (RuntimeError, "runtime error"), + (KeyError, "missing key"), +]) +def test_main_handles_execute_errors(monkeypatch, error_class, error_message): + calls = {"handle_error": None} + + def raise_error(*_args, **_kwargs): + raise error_class(error_message) + + _setup_error_mocks(monkeypatch, calls, raise_error) + monkeypatch.setattr(sys, "argv", ["skywipe", TEST_COMMAND]) + cli.main() + + assert calls["handle_error"] is not None + assert calls["handle_error"][0] == error_class.__name__ + assert calls["handle_error"][1] == error_message + assert calls["handle_error"][2] is True