From 4aa648fa8c0926c99fcd4f01bdfdcc82513dd283 Mon Sep 17 00:00:00 2001 From: sylvain Date: Tue, 10 Mar 2026 18:54:29 +0100 Subject: [PATCH] feat(cli): add main entry point with error handling (fixes #4) Read GITEA_TOKEN (required) and GITEA_URL (default) from env vars, orchestrate client/collect/render pipeline, handle connection and timeout errors gracefully. Never expose token in error messages. Co-Authored-By: Claude Opus 4.6 --- src/gitea_dashboard/cli.py | 60 ++++++++++++++++++-- tests/test_cli.py | 112 +++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 tests/test_cli.py diff --git a/src/gitea_dashboard/cli.py b/src/gitea_dashboard/cli.py index dd0813a..9ffe9a5 100644 --- a/src/gitea_dashboard/cli.py +++ b/src/gitea_dashboard/cli.py @@ -1,6 +1,58 @@ -"""Entry point for gitea-dashboard CLI.""" +"""Point d'entree pour le CLI gitea-dashboard.""" + +from __future__ import annotations + +import os +import sys + +import requests +from rich.console import Console + +from gitea_dashboard.client import GiteaClient +from gitea_dashboard.collector import collect_all +from gitea_dashboard.display import render_dashboard + +_DEFAULT_URL = "http://192.168.0.106:3000" -def main(): - """Run the dashboard.""" - pass +def main() -> None: + """Point d'entree principal. + + 1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis) + 2. Cree le GiteaClient + 3. Collecte les donnees via collect_all() + 4. Affiche via render_dashboard() + 5. Gere les erreurs : config manquante, connexion refusee, timeout + """ + console = Console(stderr=True) + + token = os.environ.get("GITEA_TOKEN") + if not token: + console.print( + "[red]Erreur : GITEA_TOKEN non defini. Exportez la variable d'environnement.[/red]" + ) + sys.exit(1) + + url = os.environ.get("GITEA_URL", _DEFAULT_URL) + + client = GiteaClient(url, token) + + try: + repos = collect_all(client) + except requests.ConnectionError: + console.print("[red]Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.[/red]") + return + except requests.Timeout: + console.print( + "[red]Erreur : delai d'attente depasse. Le serveur Gitea ne repond pas.[/red]" + ) + return + except requests.RequestException as exc: + # Ne jamais afficher le token dans les messages d'erreur + msg = str(exc) + if token in msg: + msg = msg.replace(token, "***") + console.print(f"[red]Erreur API : {msg}[/red]") + return + + render_dashboard(repos) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..31743fa --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,112 @@ +"""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) + 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_exits_when_token_missing(self): + """main() exits with SystemExit when GITEA_TOKEN is absent.""" + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(SystemExit): + main() + + def test_error_message_when_token_missing(self, capsys): + """main() prints a clear error message about missing token.""" + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(SystemExit): + main() + + +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 does not crash.""" + 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): + # Should not raise — error is handled gracefully + main() + + @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 does not crash.""" + 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): + main() + + @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 does not crash.""" + 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): + main() + + @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.""" + env = {"GITEA_TOKEN": "super-secret-token-xyz"} + mock_client_cls.return_value = MagicMock() + mock_collect.side_effect = requests.ConnectionError("Connection refused") + + with patch.dict("os.environ", env, clear=True): + main() + + captured = capsys.readouterr() + assert "super-secret-token-xyz" not in captured.out + assert "super-secret-token-xyz" not in captured.err