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 <noreply@anthropic.com>
This commit is contained in:
@@ -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():
|
def main() -> None:
|
||||||
"""Run the dashboard."""
|
"""Point d'entree principal.
|
||||||
pass
|
|
||||||
|
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)
|
||||||
|
|||||||
112
tests/test_cli.py
Normal file
112
tests/test_cli.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user