"""Tests for data collector.""" from unittest.mock import MagicMock from gitea_dashboard.collector import ( RepoData, collect_all, ) def _make_repo( name="my-repo", full_name="admin/my-repo", description="A repo", open_issues_count=5, open_pr_counter=2, fork=False, archived=False, mirror=False, owner_login="admin", ): """Build a fake repo dict as returned by the Gitea API.""" return { "name": name, "full_name": full_name, "description": description, "open_issues_count": open_issues_count, "open_pr_counter": open_pr_counter, "fork": fork, "archived": archived, "mirror": mirror, "owner": {"login": owner_login}, } class TestCollectAll: """Test collect_all function.""" def test_basic_repo(self): """Collects repo data with release and milestones.""" client = MagicMock() client.get_repos.return_value = [_make_repo()] client.get_latest_release.return_value = { "tag_name": "v1.0", "published_at": "2026-01-01", } client.get_milestones.return_value = [ {"title": "v2.0", "open_issues": 3, "closed_issues": 2, "due_on": None}, ] result = collect_all(client) assert len(result) == 1 repo = result[0] assert isinstance(repo, RepoData) assert repo.name == "my-repo" assert repo.full_name == "admin/my-repo" assert repo.description == "A repo" assert repo.open_issues == 3 # 5 - 2 assert repo.is_fork is False assert repo.is_archived is False assert repo.is_mirror is False assert repo.latest_release == {"tag_name": "v1.0", "published_at": "2026-01-01"} assert len(repo.milestones) == 1 def test_repo_without_release(self): """Repo with no release gets None for latest_release.""" client = MagicMock() client.get_repos.return_value = [_make_repo()] client.get_latest_release.return_value = None client.get_milestones.return_value = [] result = collect_all(client) assert result[0].latest_release is None def test_repo_without_milestones(self): """Repo with no milestones gets empty list.""" client = MagicMock() client.get_repos.return_value = [_make_repo()] client.get_latest_release.return_value = None client.get_milestones.return_value = [] result = collect_all(client) assert result[0].milestones == [] def test_open_issues_subtracts_prs(self): """open_issues = open_issues_count - open_pr_counter.""" client = MagicMock() client.get_repos.return_value = [ _make_repo(open_issues_count=10, open_pr_counter=4), ] client.get_latest_release.return_value = None client.get_milestones.return_value = [] result = collect_all(client) assert result[0].open_issues == 6 def test_multiple_repos(self): """Collects data for multiple repos.""" client = MagicMock() client.get_repos.return_value = [ _make_repo(name="repo-a", full_name="admin/repo-a"), _make_repo(name="repo-b", full_name="org/repo-b", owner_login="org"), ] client.get_latest_release.return_value = None client.get_milestones.return_value = [] result = collect_all(client) assert len(result) == 2 assert result[0].name == "repo-a" assert result[1].name == "repo-b" # Verify correct owner/repo passed to enrichment calls client.get_latest_release.assert_any_call("admin", "repo-a") client.get_latest_release.assert_any_call("org", "repo-b") def test_fork_and_archived_flags(self): """Fork and archived flags are propagated.""" client = MagicMock() client.get_repos.return_value = [ _make_repo(fork=True, archived=True, mirror=True), ] client.get_latest_release.return_value = None client.get_milestones.return_value = [] result = collect_all(client) assert result[0].is_fork is True assert result[0].is_archived is True assert result[0].is_mirror is True class TestCollectAllLastCommit: """Test last_commit_date field in RepoData.""" def test_repo_data_has_last_commit_date(self): """RepoData includes last_commit_date field.""" repo = RepoData( name="test", full_name="admin/test", description="", open_issues=0, is_fork=False, is_archived=False, is_mirror=False, latest_release=None, milestones=[], last_commit_date="2026-03-10T14:30:00Z", ) assert repo.last_commit_date == "2026-03-10T14:30:00Z" def test_collect_all_calls_get_latest_commit(self): """collect_all calls get_latest_commit and fills last_commit_date.""" client = MagicMock() client.get_repos.return_value = [_make_repo()] client.get_latest_release.return_value = None client.get_milestones.return_value = [] client.get_latest_commit.return_value = { "sha": "abc123", "created": "2026-03-10T14:30:00Z", } result = collect_all(client) client.get_latest_commit.assert_called_once_with("admin", "my-repo") assert result[0].last_commit_date == "2026-03-10T14:30:00Z" def test_collect_all_no_commits(self): """Repo without commits gets last_commit_date=None.""" client = MagicMock() client.get_repos.return_value = [_make_repo()] client.get_latest_release.return_value = None client.get_milestones.return_value = [] client.get_latest_commit.return_value = None result = collect_all(client) assert result[0].last_commit_date is None class TestRepoDataEdgeCases: """Test RepoData with edge case data.""" def test_repo_data_unicode_description(self): """RepoData with full unicode description (accents, CJK, emojis).""" repo = RepoData( name="unicode-test", full_name="admin/unicode-test", description="Projet avec accents : e, a, u, CJK: δΈ­ζ–‡, emojis: πŸš€πŸŽ‰", open_issues=0, is_fork=False, is_archived=False, is_mirror=False, latest_release=None, milestones=[], last_commit_date=None, ) assert "πŸš€" in repo.description assert "δΈ­ζ–‡" in repo.description def test_collect_all_repo_zero_commits_and_no_release(self): """Repo with no commits AND no release produces valid RepoData.""" client = MagicMock() client.get_repos.return_value = [_make_repo()] client.get_latest_release.return_value = None client.get_milestones.return_value = [] client.get_latest_commit.return_value = None result = collect_all(client) assert result[0].last_commit_date is None assert result[0].latest_release is None class TestCollectAllFiltering: """Test collect_all filtering (include/exclude).""" def _setup_client(self, repo_names: list[str]) -> MagicMock: """Create a mock client returning repos with the given names.""" client = MagicMock() client.get_repos.return_value = [ _make_repo(name=n, full_name=f"admin/{n}") for n in repo_names ] client.get_latest_release.return_value = None client.get_milestones.return_value = [] return client def test_no_filter_returns_all(self): """Without include/exclude, all repos are returned (backward compat).""" client = self._setup_client(["alpha", "beta", "gamma"]) result = collect_all(client) assert [r.name for r in result] == ["alpha", "beta", "gamma"] def test_include_single(self): """Include filters repos by substring match.""" client = self._setup_client(["gitea-dashboard", "infra-core", "notes"]) result = collect_all(client, include=["dashboard"]) assert [r.name for r in result] == ["gitea-dashboard"] def test_include_multiple(self): """Multiple include patterns are OR-combined.""" client = self._setup_client(["gitea-dashboard", "infra-core", "notes"]) result = collect_all(client, include=["dashboard", "infra"]) assert [r.name for r in result] == ["gitea-dashboard", "infra-core"] def test_exclude_single(self): """Exclude removes repos matching the substring.""" client = self._setup_client(["gitea-dashboard", "old-fork", "notes"]) result = collect_all(client, exclude=["fork"]) assert [r.name for r in result] == ["gitea-dashboard", "notes"] def test_include_and_exclude(self): """Include is applied first, then exclude.""" client = self._setup_client(["projet-web", "projet-old", "infra"]) result = collect_all(client, include=["projet"], exclude=["old"]) assert [r.name for r in result] == ["projet-web"] def test_case_insensitive(self): """Filtering is case-insensitive.""" client = self._setup_client(["Gitea-Dashboard", "infra"]) result = collect_all(client, include=["dashboard"]) assert [r.name for r in result] == ["Gitea-Dashboard"] def test_no_match_returns_empty(self): """Returns empty list when no repo matches include filter.""" client = self._setup_client(["alpha", "beta"]) result = collect_all(client, include=["inexistant"]) assert result == [] def test_exclude_all_returns_empty(self): """Returns empty list when all repos are excluded.""" client = self._setup_client(["alpha", "beta"]) result = collect_all(client, exclude=["alpha", "beta"]) assert result == [] def test_filtered_repos_have_no_api_calls(self): """Repos excluded by include filter must not trigger enrichment API calls.""" client = self._setup_client(["gitea-dashboard", "infra-core", "notes"]) collect_all(client, include=["dashboard"]) # Only gitea-dashboard passed the filter β€” enrichment calls must target it only client.get_latest_release.assert_called_once_with("admin", "gitea-dashboard") client.get_milestones.assert_called_once_with("admin", "gitea-dashboard") def test_collect_all_include_empty_list(self): """include=[] behaves like include=None β€” all repos are returned. The contract: an empty list is falsy, so `if include:` is False, meaning no inclusion filter is applied and every repo is included before exclude. """ client = self._setup_client(["alpha", "beta", "gamma"]) result_none = collect_all(client) result_empty = collect_all(client, include=[]) assert [r.name for r in result_empty] == [r.name for r in result_none] class TestCollectMilestones: """Test collect_milestones function.""" def _setup_client(self, repo_names, milestones_by_repo=None): """Create a mock client with repos and milestones.""" client = MagicMock() client.get_repos.return_value = [ _make_repo(name=n, full_name=f"admin/{n}") for n in repo_names ] if milestones_by_repo is None: milestones_by_repo = {} def get_milestones_side_effect(owner, repo, state="all"): return milestones_by_repo.get(repo, []) client.get_milestones.side_effect = get_milestones_side_effect return client def test_collect_milestones_basic(self): """2 repos with milestones returns flat list of MilestoneData.""" from gitea_dashboard.collector import MilestoneData, collect_milestones client = self._setup_client( ["repo-a", "repo-b"], { "repo-a": [ { "title": "v1.0", "open_issues": 2, "closed_issues": 3, "due_on": None, "state": "open", }, ], "repo-b": [ { "title": "v2.0", "open_issues": 0, "closed_issues": 5, "due_on": "2026-04-01T00:00:00Z", "state": "closed", }, ], }, ) result = collect_milestones(client) assert len(result) == 2 assert all(isinstance(m, MilestoneData) for m in result) assert result[0].repo_name == "repo-a" assert result[0].title == "v1.0" assert result[1].repo_name == "repo-b" def test_collect_milestones_empty_repo(self): """Repo without milestones produces no entries.""" from gitea_dashboard.collector import collect_milestones client = self._setup_client(["empty-repo"], {"empty-repo": []}) result = collect_milestones(client) assert result == [] def test_collect_milestones_progress_calculation(self): """3 open + 7 closed = progress_pct 70.""" from gitea_dashboard.collector import collect_milestones client = self._setup_client( ["repo"], { "repo": [ { "title": "v1.0", "open_issues": 3, "closed_issues": 7, "due_on": None, "state": "open", }, ], }, ) result = collect_milestones(client) assert result[0].progress_pct == 70 def test_collect_milestones_with_include_filter(self): """Include filter is respected.""" from gitea_dashboard.collector import collect_milestones client = self._setup_client( ["gitea-dashboard", "infra"], { "gitea-dashboard": [ { "title": "v1.0", "open_issues": 1, "closed_issues": 1, "due_on": None, "state": "open", }, ], "infra": [ { "title": "v2.0", "open_issues": 0, "closed_issues": 5, "due_on": None, "state": "closed", }, ], }, ) result = collect_milestones(client, include=["dashboard"]) assert len(result) == 1 assert result[0].repo_name == "gitea-dashboard" def test_collect_milestones_with_exclude_filter(self): """Exclude filter is respected.""" from gitea_dashboard.collector import collect_milestones client = self._setup_client( ["gitea-dashboard", "old-fork"], { "gitea-dashboard": [ { "title": "v1.0", "open_issues": 1, "closed_issues": 1, "due_on": None, "state": "open", }, ], "old-fork": [ { "title": "v2.0", "open_issues": 0, "closed_issues": 5, "due_on": None, "state": "closed", }, ], }, ) result = collect_milestones(client, exclude=["fork"]) assert len(result) == 1 assert result[0].repo_name == "gitea-dashboard"