- client: add raise_for_status() in get_latest_release() for non-404 errors (FINDING-001) - client: add timeout parameter (default 30s) passed to all session.get() calls (FINDING-004/SEC-002) - cli: replace return with sys.exit(1) in all except blocks (FINDING-002) - test_cli: remove duplicate test_exits_when_token_missing, assert GITEA_TOKEN in stderr (FINDING-006) - test_cli: update connection error tests to expect SystemExit(1) after exit code fix - test_cli: rework token masking test to inject token into exception message (FINDING-007) - test_client: add test_raises_on_server_error for HTTP 500 path (FINDING-001) - test_client: add tests for default and custom timeout values (FINDING-004) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
"""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_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
|