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:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user