diff --git a/src/gitea_dashboard/cli.py b/src/gitea_dashboard/cli.py index f5a775b..f70bb15 100644 --- a/src/gitea_dashboard/cli.py +++ b/src/gitea_dashboard/cli.py @@ -110,6 +110,10 @@ def _resolve_config(args: argparse.Namespace) -> argparse.Namespace: """ 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 = {} gitea_url_env = os.environ.get("GITEA_URL") if gitea_url_env: @@ -209,6 +213,14 @@ def main(argv: list[str] | None = None) -> None: ) 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 = ( args.resolved_url if hasattr(args, "resolved_url") diff --git a/src/gitea_dashboard/client.py b/src/gitea_dashboard/client.py index 19f17cf..9bcb0a8 100644 --- a/src/gitea_dashboard/client.py +++ b/src/gitea_dashboard/client.py @@ -107,7 +107,8 @@ class GiteaClient: if page == 1: raise 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, ) return all_items diff --git a/src/gitea_dashboard/display.py b/src/gitea_dashboard/display.py index 2df2795..697acc5 100644 --- a/src/gitea_dashboard/display.py +++ b/src/gitea_dashboard/display.py @@ -262,6 +262,12 @@ def render_dashboard( if repo.last_commit_date 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) console.print(table) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7ad5f6e..6d6b9c0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -411,6 +411,42 @@ class TestParseArgsMilestones: 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: """Test --columns argument parsing.""" @@ -453,7 +489,9 @@ class TestMainColumnsHelp: @patch("gitea_dashboard.cli.GiteaClient") 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"} mock_client_cls.return_value = MagicMock() @@ -461,8 +499,12 @@ class TestMainColumnsHelp: main(["--columns", "help"]) captured = capsys.readouterr() - # Should list column names - assert "name" in captured.out or "name" in captured.err + combined = captured.out + 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.collect_all") diff --git a/tests/test_display.py b/tests/test_display.py index 091545c..3605176 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -529,3 +529,29 @@ class TestParseColumns: assert "test" in output assert "Description" 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