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:
sylvain
2026-03-10 18:54:29 +01:00
parent 8fbdfcafd4
commit 4aa648fa8c
2 changed files with 168 additions and 4 deletions

112
tests/test_cli.py Normal file
View 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