"""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, sort_key="name", show_description=True ) @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) class TestParseArgsSort: """Test --sort argument parsing.""" def test_sort_default(self): """Without --sort, default is 'name'.""" from gitea_dashboard.cli import parse_args args = parse_args([]) assert args.sort == "name" def test_sort_issues(self): """--sort issues is accepted.""" from gitea_dashboard.cli import parse_args args = parse_args(["--sort", "issues"]) assert args.sort == "issues" def test_sort_short_flag(self): """-s activity is accepted.""" from gitea_dashboard.cli import parse_args args = parse_args(["-s", "activity"]) assert args.sort == "activity" def test_sort_invalid(self): """--sort invalid raises SystemExit (argparse error).""" from gitea_dashboard.cli import parse_args with pytest.raises(SystemExit): parse_args(["--sort", "invalid"]) class TestParseArgsFormat: """Test --format argument parsing.""" def test_format_default(self): """Without --format, default is 'table'.""" from gitea_dashboard.cli import parse_args args = parse_args([]) assert args.format == "table" def test_format_json(self): """--format json is accepted.""" from gitea_dashboard.cli import parse_args args = parse_args(["--format", "json"]) assert args.format == "json" def test_format_short_flag(self): """-f json is accepted.""" from gitea_dashboard.cli import parse_args args = parse_args(["-f", "json"]) assert args.format == "json" def test_format_invalid(self): """--format invalid raises SystemExit.""" from gitea_dashboard.cli import parse_args with pytest.raises(SystemExit): parse_args(["--format", "invalid"]) class TestParseArgsHealth: """Test --health argument parsing.""" def test_parse_args_health(self): """--health sets health=True.""" from gitea_dashboard.cli import parse_args args = parse_args(["--health"]) assert args.health is True def test_parse_args_no_health_default(self): """Without --health, health is False.""" from gitea_dashboard.cli import parse_args args = parse_args([]) assert args.health is False class TestParseArgsNoDesc: """Test --no-desc argument parsing.""" def test_parse_args_no_desc(self): """--no-desc sets no_desc=True.""" from gitea_dashboard.cli import parse_args args = parse_args(["--no-desc"]) assert args.no_desc is True def test_parse_args_no_desc_default(self): """Without --no-desc, no_desc is False.""" from gitea_dashboard.cli import parse_args args = parse_args([]) assert args.no_desc is False class TestMainHealth: """Test main() with --health.""" @patch("gitea_dashboard.cli.GiteaClient") def test_main_health_success(self, mock_client_cls, capsys): """--health displays version and repo count, exits normally.""" env = {"GITEA_TOKEN": "test-token"} mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.get_version.return_value = {"version": "1.21.0"} mock_client.get_repos.return_value = [{"id": 1}, {"id": 2}, {"id": 3}] with patch.dict("os.environ", env, clear=True): main(["--health"]) captured = capsys.readouterr() assert "Gitea v1.21.0" in captured.err assert "3 repos accessibles" in captured.err @patch("gitea_dashboard.cli.GiteaClient") def test_main_health_connection_error(self, mock_client_cls): """--health with connection error exits with code 1.""" env = {"GITEA_TOKEN": "test-token"} mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.get_version.side_effect = requests.ConnectionError("refused") with patch.dict("os.environ", env, clear=True): with pytest.raises(SystemExit) as exc_info: main(["--health"]) assert exc_info.value.code == 1 @patch("gitea_dashboard.cli.GiteaClient") def test_main_health_version_ok_repos_fail(self, mock_client_cls): """--health : get_version reussit mais get_repos leve HTTPError -> exit 1. Verifie le cas d'un health check partiel : l'instance Gitea repond sur /version mais l'acces aux repos echoue (ex: token sans permissions). """ env = {"GITEA_TOKEN": "test-token"} mock_client = MagicMock() mock_client_cls.return_value = mock_client mock_client.get_version.return_value = {"version": "1.21.0"} mock_client.get_repos.side_effect = requests.HTTPError("403 Forbidden") with patch.dict("os.environ", env, clear=True): with pytest.raises(SystemExit) as exc_info: main(["--health"]) assert exc_info.value.code == 1 class TestMainNoDesc: """Test main() with --no-desc.""" @patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_main_passes_no_desc_to_render(self, mock_client_cls, mock_collect, mock_render): """--no-desc passes show_description=False to render_dashboard.""" env = {"GITEA_TOKEN": "test-token"} mock_client_cls.return_value = MagicMock() mock_collect.return_value = [] with patch.dict("os.environ", env, clear=True): main(["--no-desc"]) mock_render.assert_called_once_with( mock_collect.return_value, sort_key="name", show_description=False ) class TestMainFormatJson: """Test main() with --format json.""" @patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.GiteaClient") def test_json_output(self, mock_client_cls, mock_collect, capsys): """--format json produces valid JSON on stdout.""" import json env = {"GITEA_TOKEN": "test-token"} mock_client_cls.return_value = MagicMock() mock_collect.return_value = [] with patch.dict("os.environ", env, clear=True): main(["--format", "json"]) captured = capsys.readouterr() parsed = json.loads(captured.out) assert isinstance(parsed, list)