fix(audit): correct v1.4.0 findings (6 items)

- FINDING-001: add activity column rendering in render_dashboard loop
- FINDING-002: map YAML 'token' key to 'auth' in _resolve_config
- FINDING-003/SEC-001: reject tokens containing unresolved ${...} refs
- FINDING-004: add tests for activity column rendering
- FINDING-006: strengthen test_main_columns_help assertions
- SEC-002: enrich timeout warning with collected items count

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-13 03:58:38 +01:00
parent 6f2f02409e
commit e02e211d86
5 changed files with 91 additions and 4 deletions

View File

@@ -110,6 +110,10 @@ def _resolve_config(args: argparse.Namespace) -> argparse.Namespace:
""" """
file_config = load_config(args.config) file_config = load_config(args.config)
# Map YAML key "token" to internal key "auth" for merge consistency
if "token" in file_config:
file_config["auth"] = file_config.pop("token")
env_vars: dict = {} env_vars: dict = {}
gitea_url_env = os.environ.get("GITEA_URL") gitea_url_env = os.environ.get("GITEA_URL")
if gitea_url_env: if gitea_url_env:
@@ -209,6 +213,14 @@ def main(argv: list[str] | None = None) -> None:
) )
sys.exit(1) sys.exit(1)
# Detect unresolved ${VAR} references in token (SEC-001)
if "${" in auth:
console.print(
"[red]Erreur : le token contient une reference ${...} non resolue. "
"Verifiez que la variable d'environnement est definie.[/red]"
)
sys.exit(1)
url = ( url = (
args.resolved_url args.resolved_url
if hasattr(args, "resolved_url") if hasattr(args, "resolved_url")

View File

@@ -107,7 +107,8 @@ class GiteaClient:
if page == 1: if page == 1:
raise raise
warnings.warn( warnings.warn(
f"Partial data: timeout on page {page} of {endpoint}", f"Partial data: timeout on page {page} of {endpoint} "
f"(collected {len(all_items)} items so far)",
stacklevel=2, stacklevel=2,
) )
return all_items return all_items

View File

@@ -262,6 +262,12 @@ def render_dashboard(
if repo.last_commit_date if repo.last_commit_date
else "\u2014" else "\u2014"
) )
elif col == "activity":
row.append(
_format_relative_date(repo.last_commit_date)
if repo.last_commit_date
else "\u2014"
)
table.add_row(*row) table.add_row(*row)
console.print(table) console.print(table)

View File

@@ -411,6 +411,42 @@ class TestParseArgsMilestones:
assert args.milestones is False assert args.milestones is False
class TestMainTokenFromConfig:
"""Test main() reads token from YAML config file."""
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
@patch("gitea_dashboard.cli.load_config")
def test_yaml_token_key_mapped_to_auth(
self, mock_load_config, mock_client_cls, mock_collect, mock_render
):
"""YAML 'token' key is properly mapped to auth for GiteaClient."""
mock_load_config.return_value = {"token": "yaml-token-123", "url": "http://yaml:3000"}
mock_client_cls.return_value = MagicMock()
mock_collect.return_value = []
with patch.dict("os.environ", {}, clear=True):
main([])
mock_client_cls.assert_called_once_with("http://yaml:3000", "yaml-token-123")
class TestMainUnresolvedToken:
"""Test main() rejects unresolved ${VAR} in token."""
def test_unresolved_env_var_in_token(self, capsys):
"""Token containing ${...} is rejected with clear error."""
env = {"GITEA_TOKEN": "${GITEA_TOKEN}"}
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main([])
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "${" in captured.err
class TestParseArgsColumns: class TestParseArgsColumns:
"""Test --columns argument parsing.""" """Test --columns argument parsing."""
@@ -453,7 +489,9 @@ class TestMainColumnsHelp:
@patch("gitea_dashboard.cli.GiteaClient") @patch("gitea_dashboard.cli.GiteaClient")
def test_main_columns_help(self, mock_client_cls, capsys): def test_main_columns_help(self, mock_client_cls, capsys):
"""--columns help displays available columns and exits.""" """--columns help displays ALL available columns and does not instantiate client."""
from gitea_dashboard.display import AVAILABLE_COLUMNS
env = {"GITEA_TOKEN": "test-tok"} env = {"GITEA_TOKEN": "test-tok"}
mock_client_cls.return_value = MagicMock() mock_client_cls.return_value = MagicMock()
@@ -461,8 +499,12 @@ class TestMainColumnsHelp:
main(["--columns", "help"]) main(["--columns", "help"])
captured = capsys.readouterr() captured = capsys.readouterr()
# Should list column names combined = captured.out + captured.err
assert "name" in captured.out or "name" in captured.err # Every column key must appear in the output
for col_name in AVAILABLE_COLUMNS:
assert col_name in combined, f"Column '{col_name}' missing from --columns help output"
# GiteaClient should NOT have been instantiated (help exits early)
mock_client_cls.assert_not_called()
@patch("gitea_dashboard.cli.render_dashboard") @patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all") @patch("gitea_dashboard.cli.collect_all")

View File

@@ -529,3 +529,29 @@ class TestParseColumns:
assert "test" in output assert "test" in output
assert "Description" not in output assert "Description" not in output
assert "Release" not in output assert "Release" not in output
def test_render_dashboard_activity_column(self):
"""Activity column renders relative date from last_commit_date."""
from gitea_dashboard.display import render_dashboard
console, buf = _make_console()
repos = [_make_repo(name="active-repo", last_commit_date="2026-03-10T14:30:00Z")]
render_dashboard(repos, console=console, columns=["name", "activity"])
output = buf.getvalue()
assert "Activite" in output
assert "active-repo" in output
assert "il y a" in output or "aujourd'hui" in output
def test_render_dashboard_activity_column_no_commit(self):
"""Activity column shows dash when no commit date."""
from gitea_dashboard.display import render_dashboard
console, buf = _make_console()
repos = [_make_repo(name="empty-repo", last_commit_date=None)]
render_dashboard(repos, console=console, columns=["name", "activity"])
output = buf.getvalue()
assert "\u2014" in output or "\u2014" in output