Files
gitea-dashboard/tests/test_collector.py
sylvain 2ef7ec175e test: add edge case tests for unicode, empty repos, malformed API
Add tests for unicode descriptions, repos with no commits and no
release, malformed JSON responses, HTML responses, control characters
in names, empty and very long descriptions.

fixes #13

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:16:06 +01:00

314 lines
11 KiB
Python

"""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]