_get_with_retry now retries on HTTP 429 responses, respecting the Retry-After header when present. exporter sanitizes control characters (0x00-0x1F except \n \r \t) in text fields before JSON serialization. fixes #11 fixes #12 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""Tests for JSON exporter module."""
|
|
|
|
import json
|
|
|
|
from gitea_dashboard.exporter import export_json, repos_to_dicts
|
|
|
|
from tests.helpers import make_repo as _make_repo
|
|
|
|
|
|
class TestReposToDicts:
|
|
"""Test repos_to_dicts function."""
|
|
|
|
def test_basic_conversion(self):
|
|
"""Converts a RepoData to dict with correct values."""
|
|
repo = _make_repo(name="test-repo", open_issues=5)
|
|
result = repos_to_dicts([repo])
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["name"] == "test-repo"
|
|
assert result[0]["open_issues"] == 5
|
|
|
|
def test_empty_list(self):
|
|
"""Empty input returns empty list."""
|
|
assert repos_to_dicts([]) == []
|
|
|
|
def test_preserves_all_fields(self):
|
|
"""All RepoData fields are present in the output dict."""
|
|
repo = _make_repo(
|
|
name="full",
|
|
full_name="admin/full",
|
|
description="desc",
|
|
open_issues=2,
|
|
is_fork=True,
|
|
is_archived=False,
|
|
is_mirror=True,
|
|
latest_release={"tag_name": "v1.0", "published_at": "2026-01-01"},
|
|
milestones=[{"title": "v2.0"}],
|
|
last_commit_date="2026-03-10T00:00:00Z",
|
|
)
|
|
result = repos_to_dicts([repo])
|
|
d = result[0]
|
|
|
|
expected_fields = [
|
|
"name",
|
|
"full_name",
|
|
"description",
|
|
"open_issues",
|
|
"is_fork",
|
|
"is_archived",
|
|
"is_mirror",
|
|
"latest_release",
|
|
"milestones",
|
|
"last_commit_date",
|
|
]
|
|
for field in expected_fields:
|
|
assert field in d, f"Missing field: {field}"
|
|
|
|
|
|
class TestSanitizeControlChars:
|
|
"""Test control character sanitization in export."""
|
|
|
|
def test_export_json_sanitizes_control_chars(self):
|
|
"""Description with control chars (0x00, 0x01, 0x02) produces valid JSON without them."""
|
|
repo = _make_repo(description="hello\x00\x01\x02world")
|
|
output = export_json([repo])
|
|
|
|
parsed = json.loads(output)
|
|
assert parsed[0]["description"] == "helloworld"
|
|
|
|
def test_export_json_preserves_newlines_tabs(self):
|
|
"""Newlines and tabs are preserved in JSON export (they are valid JSON escapes)."""
|
|
repo = _make_repo(description="line1\nline2\ttab")
|
|
output = export_json([repo])
|
|
|
|
parsed = json.loads(output)
|
|
assert parsed[0]["description"] == "line1\nline2\ttab"
|
|
|
|
def test_export_json_unicode_safe(self):
|
|
"""Description with emojis and accents produces valid JSON."""
|
|
repo = _make_repo(description="Projet avec accents : e, a et emojis 🚀🎉")
|
|
output = export_json([repo])
|
|
|
|
parsed = json.loads(output)
|
|
assert "🚀" in parsed[0]["description"]
|
|
assert "accents" in parsed[0]["description"]
|
|
|
|
def test_sanitize_name_and_full_name(self):
|
|
"""Control chars in name and full_name fields are also sanitized."""
|
|
repo = _make_repo(name="test\x00repo", full_name="admin/test\x01repo")
|
|
result = repos_to_dicts([repo])
|
|
|
|
assert result[0]["name"] == "testrepo"
|
|
assert result[0]["full_name"] == "admin/testrepo"
|
|
|
|
|
|
class TestExportJson:
|
|
"""Test export_json function."""
|
|
|
|
def test_valid_json(self):
|
|
"""Output is valid JSON (json.loads does not raise)."""
|
|
repos = [_make_repo(name="repo-a"), _make_repo(name="repo-b")]
|
|
output = export_json(repos)
|
|
|
|
parsed = json.loads(output)
|
|
assert isinstance(parsed, list)
|
|
assert len(parsed) == 2
|
|
|
|
def test_indented(self):
|
|
"""JSON output is indented by default."""
|
|
repos = [_make_repo()]
|
|
output = export_json(repos)
|
|
|
|
# Indented JSON has newlines and spaces
|
|
assert "\n" in output
|
|
assert " " in output
|
|
|
|
def test_empty_list(self):
|
|
"""Empty repo list produces '[]'."""
|
|
output = export_json([])
|
|
assert json.loads(output) == []
|