"""Tests for CLI entry point.""" from unittest.mock import MagicMock, patch import pytest import requests from gitea_dashboard.cli import main class TestMainNominal: """Test main() happy path.""" @patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_main_runs_full_pipeline(self, mock_client_cls, mock_collect, mock_render): """main() creates client, collects, and renders.""" env = {"GITEA_TOKEN": "test-token-123", "GITEA_URL": "http://localhost:3000"} mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_collect.return_value = [] with patch.dict("os.environ", env, clear=False): main([]) mock_client_cls.assert_called_once_with("http://localhost:3000", "test-token-123") mock_collect.assert_called_once_with(mock_client, include=None, exclude=None) mock_render.assert_called_once_with(mock_collect.return_value) @patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_main_uses_default_url(self, mock_client_cls, mock_collect, mock_render): """main() uses default URL when GITEA_URL is not set.""" env = {"GITEA_TOKEN": "my-token"} mock_client_cls.return_value = MagicMock() mock_collect.return_value = [] with patch.dict("os.environ", env, clear=True): main([]) mock_client_cls.assert_called_once_with("http://192.168.0.106:3000", "my-token") class TestMainMissingToken: """Test main() when GITEA_TOKEN is not set.""" def test_error_message_when_token_missing(self, capsys): """main() exits with code 1 and prints message mentioning GITEA_TOKEN.""" with patch.dict("os.environ", {}, clear=True): with pytest.raises(SystemExit) as exc_info: main([]) assert exc_info.value.code == 1 captured = capsys.readouterr() assert "GITEA_TOKEN" in captured.err class TestMainConnectionErrors: """Test main() error handling for network issues.""" @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_connection_error_handled(self, mock_client_cls, mock_collect): """ConnectionError is caught and exits with code 1.""" env = {"GITEA_TOKEN": "test-token"} mock_client_cls.return_value = MagicMock() mock_collect.side_effect = requests.ConnectionError("Connection refused") with patch.dict("os.environ", env, clear=True): with pytest.raises(SystemExit) as exc_info: main([]) assert exc_info.value.code == 1 @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_timeout_error_handled(self, mock_client_cls, mock_collect): """Timeout is caught and exits with code 1.""" env = {"GITEA_TOKEN": "test-token"} mock_client_cls.return_value = MagicMock() mock_collect.side_effect = requests.Timeout("Request timed out") with patch.dict("os.environ", env, clear=True): with pytest.raises(SystemExit) as exc_info: main([]) assert exc_info.value.code == 1 @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_request_exception_handled(self, mock_client_cls, mock_collect): """Generic RequestException is caught and exits with code 1.""" env = {"GITEA_TOKEN": "test-token"} mock_client_cls.return_value = MagicMock() mock_collect.side_effect = requests.RequestException("Something went wrong") with patch.dict("os.environ", env, clear=True): with pytest.raises(SystemExit) as exc_info: main([]) assert exc_info.value.code == 1 @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_token_not_in_error_output(self, mock_client_cls, mock_collect, capsys): """Token must never appear in error messages, even when present in the exception.""" env = {"GITEA_TOKEN": "super-secret-token-xyz"} mock_client_cls.return_value = MagicMock() # Build exception message that embeds the token value from env # to simulate a real-world leak scenario def make_exc(environ): leaked = environ["GITEA_TOKEN"] return requests.RequestException(f"HTTP Error: Authorization token {leaked} rejected") import os as _os with patch.dict("os.environ", env, clear=True): mock_collect.side_effect = make_exc(_os.environ) with pytest.raises(SystemExit): main([]) captured = capsys.readouterr() assert env["GITEA_TOKEN"] not in captured.out assert env["GITEA_TOKEN"] not in captured.err class TestParseArgs: """Test parse_args function.""" def test_no_options(self): """No arguments returns None for both repo and exclude.""" from gitea_dashboard.cli import parse_args args = parse_args([]) assert args.repo is None assert args.exclude is None def test_single_repo(self): """--repo foo returns repo=["foo"].""" from gitea_dashboard.cli import parse_args args = parse_args(["--repo", "foo"]) assert args.repo == ["foo"] def test_multiple_repo(self): """--repo foo --repo bar returns repo=["foo", "bar"].""" from gitea_dashboard.cli import parse_args args = parse_args(["--repo", "foo", "--repo", "bar"]) assert args.repo == ["foo", "bar"] def test_short_flags(self): """-r foo -x bar works like long forms.""" from gitea_dashboard.cli import parse_args args = parse_args(["-r", "foo", "-x", "bar"]) assert args.repo == ["foo"] assert args.exclude == ["bar"] class TestMainWithFilters: """Test main() passes filters to collect_all.""" @patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_main_passes_filters(self, mock_client_cls, mock_collect, mock_render): """main() passes include/exclude from CLI args to collect_all.""" env = {"GITEA_TOKEN": "test-token"} mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_collect.return_value = [] with patch.dict("os.environ", env, clear=True): main(["--repo", "dash", "--exclude", "old"]) mock_collect.assert_called_once_with(mock_client, include=["dash"], exclude=["old"]) @patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_main_no_filters_passes_none(self, mock_client_cls, mock_collect, mock_render): """Without options, collect_all is called with include=None, exclude=None.""" env = {"GITEA_TOKEN": "test-token"} mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_collect.return_value = [] with patch.dict("os.environ", env, clear=True): main([]) mock_collect.assert_called_once_with(mock_client, include=None, exclude=None)