From 2ef7ec175e5c0775e297e0f236ea1c00d4d1c046 Mon Sep 17 00:00:00 2001 From: sylvain Date: Thu, 12 Mar 2026 19:16:06 +0100 Subject: [PATCH] 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 --- tests/test_client.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_collector.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_display.py | 25 +++++++++++++++++++++++++ tests/test_exporter.py | 20 ++++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 90a7201..63b4496 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -287,6 +287,41 @@ class TestGetWithRetry429: assert mock_sleep.call_count == 2 +class TestGetPaginatedEdgeCases: + """Test edge cases for API responses.""" + + def _make_client(self): + return GiteaClient("http://gitea.local:3000", "tok") + + def test_get_paginated_malformed_json(self): + """Response with invalid JSON raises JSONDecodeError.""" + import json + + client = self._make_client() + mock_resp = MagicMock() + mock_resp.raise_for_status = MagicMock() + mock_resp.json.side_effect = json.JSONDecodeError("Expecting value", "", 0) + + with patch.object(client.session, "get", return_value=mock_resp): + with pytest.raises(json.JSONDecodeError): + client._get_paginated("/api/v1/user/repos") + + def test_get_repos_html_response(self): + """HTML response (status 200 but HTML content) raises on json parsing.""" + import json + + client = self._make_client() + mock_resp = MagicMock() + mock_resp.raise_for_status = MagicMock() + mock_resp.json.side_effect = json.JSONDecodeError( + "Expecting value", "Maintenance", 0 + ) + + with patch.object(client.session, "get", return_value=mock_resp): + with pytest.raises(json.JSONDecodeError): + client.get_repos() + + class TestGetLatestCommit: """Test get_latest_commit method.""" diff --git a/tests/test_collector.py b/tests/test_collector.py index a8e1f8c..3e4f9e5 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -178,6 +178,40 @@ class TestCollectAllLastCommit: 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).""" diff --git a/tests/test_display.py b/tests/test_display.py index 3dd15de..b48e69a 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -230,6 +230,31 @@ class TestRenderDashboardEmpty: assert "Aucun repo" in output +class TestRenderDashboardEdgeCases: + """Test edge cases for dashboard rendering.""" + + def test_render_dashboard_unicode_description(self): + """Repo with unicode description renders without crash.""" + console, buf = _make_console() + repos = [_make_repo(name="unicode", description="Projet πŸš€ avec accents eaiu δΈ­ζ–‡")] + + render_dashboard(repos, console=console) + output = buf.getvalue() + + assert "unicode" in output + + def test_render_dashboard_control_chars_in_name(self): + """Repo with control characters in name renders without crash.""" + console, buf = _make_console() + repos = [_make_repo(name="test\x00repo")] + + render_dashboard(repos, console=console) + output = buf.getvalue() + + # Rich may strip or display the control char, but must not crash + assert "test" in output + + class TestColorizeMilestoneDue: """Test _colorize_milestone_due function.""" diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 506c0a7..0a40bc9 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -93,6 +93,26 @@ class TestSanitizeControlChars: assert result[0]["full_name"] == "admin/testrepo" +class TestExportJsonEdgeCases: + """Test edge cases for JSON export.""" + + def test_export_json_empty_description(self): + """Empty description produces valid JSON.""" + repo = _make_repo(description="") + output = export_json([repo]) + + parsed = json.loads(output) + assert parsed[0]["description"] == "" + + def test_export_json_very_long_description(self): + """Very long description (10000 chars) produces valid JSON.""" + repo = _make_repo(description="x" * 10000) + output = export_json([repo]) + + parsed = json.loads(output) + assert len(parsed[0]["description"]) == 10000 + + class TestExportJson: """Test export_json function."""