diff --git a/src/revenueholdings/cli.py b/src/revenueholdings/cli.py index 6decad1..dc07416 100644 --- a/src/revenueholdings/cli.py +++ b/src/revenueholdings/cli.py @@ -120,7 +120,11 @@ def show_versions( tool: str | None = typer.Argument(None, help="Check version of a specific tool."), ): """Show installed tool versions.""" - targets = ([tool] if tool in TOOLS else []) if tool else _builtins.list(TOOLS.keys()) + if tool is not None and tool not in TOOLS: + console.print(f"[red]Unknown tool: {tool}[/red]") + console.print(f"Available: {', '.join(TOOLS.keys())}") + raise typer.Exit(code=1) + targets = [tool] if tool else _builtins.list(TOOLS.keys()) for t in targets: info = TOOLS[t] diff --git a/tests/test_cli.py b/tests/test_cli.py index 9a45833..6d76c55 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,6 +4,7 @@ from revenueholdings import TOOLS, __version__ from revenueholdings.cli import app from typer.testing import CliRunner +from unittest import mock runner = CliRunner() @@ -35,12 +36,73 @@ def test_unknown_tool(self): assert "Unknown" in result.stdout +class TestInstallCommand: + @mock.patch("revenueholdings.cli.subprocess.run") + def test_install_specific_tool(self, mock_run): + """Install a specific tool by name.""" + mock_run.return_value = mock.MagicMock(returncode=0, stdout="", stderr="") + result = runner.invoke(app, ["install", "guard"]) + assert result.exit_code == 0 + assert "Successfully" in result.stdout + mock_run.assert_called_once() + + @mock.patch("revenueholdings.cli.subprocess.run") + def test_install_all(self, mock_run): + """Install all tools via the 'all' alias.""" + mock_run.return_value = mock.MagicMock(returncode=0, stdout="", stderr="") + result = runner.invoke(app, ["install", "all"]) + assert result.exit_code == 0 + assert "Successfully" in result.stdout + mock_run.assert_called_once() + + def test_install_unknown_tool(self): + """Error on unknown tool name.""" + result = runner.invoke(app, ["install", "nonexistent"]) + assert result.exit_code == 1 + assert "Unknown" in result.stdout + assert "Available:" in result.stdout + + @mock.patch("revenueholdings.cli.subprocess.run") + def test_install_failure(self, mock_run): + """Handle pip install failure gracefully.""" + mock_run.return_value = mock.MagicMock(returncode=1, stdout="", stderr="Error message") + result = runner.invoke(app, ["install", "guard"]) + assert result.exit_code == 1 + assert "failed" in result.stdout.lower() + + class TestVersionsCommand: def test_versions_runs(self): + """List all tool versions without error.""" result = runner.invoke(app, ["versions"]) - # Should succeed even if tools aren't installed assert result.exit_code == 0 + def test_versions_unknown_tool_fails(self): + """Error on unknown tool name.""" + result = runner.invoke(app, ["versions", "nonexistent"]) + assert result.exit_code == 1 + assert "Unknown" in result.stdout + + @mock.patch("revenueholdings.cli.subprocess.run") + def test_versions_specific_tool_not_installed(self, mock_run): + """Show 'not installed' for a tool that isn't installed.""" + mock_run.return_value = mock.MagicMock( + returncode=1, stdout="", stderr="" + ) + result = runner.invoke(app, ["versions", "guard"]) + assert result.exit_code == 0 + assert "guard" in result.stdout + assert "not installed" in result.stdout + + +class TestDispatchCommands: + def test_invalid_tool_subcommand(self): + """Reject dispatch to an unknown tool subcommand.""" + result = runner.invoke(app, ["nonexistent"]) + # typer outputs error to stderr, not stdout + assert result.exit_code != 0 + assert "No such command" in result.stdout or "No such command" in result.stderr + class TestHelp: def test_help(self):