Files
gitea-dashboard/tests/test_client.py
sylvain 4c66fbe98d feat(v1.2.0): retry API, dernier commit, tri, coloration, export JSON
- client.py: _get_with_retry (max 2 retries, backoff lineaire), get_latest_commit
- collector.py: champ last_commit_date dans RepoData
- display.py: colonne "Dernier commit", _sort_repos (name/issues/release/activity),
  _colorize_milestone_due (rouge/jaune/vert selon echeance)
- cli.py: options --sort/-s et --format/-f (table/json)
- exporter.py: nouveau module, repos_to_dicts + export_json
- 88 tests (35 nouveaux), ruff clean

fixes #8, fixes #7, fixes #10, fixes #9, fixes #6

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

251 lines
9.1 KiB
Python

"""Tests for GiteaClient API client."""
from unittest.mock import MagicMock, patch
import pytest
import requests
from gitea_dashboard.client import GiteaClient
class TestGiteaClientInit:
"""Test client initialization and auth."""
def test_auth_header_is_set(self):
"""Session must carry Authorization header with token prefix."""
client = GiteaClient("http://gitea.local:3000", "my-secret-token")
assert client.session.headers["Authorization"] == "token my-secret-token"
def test_base_url_stored(self):
"""Base URL is stored without trailing slash."""
client = GiteaClient("http://gitea.local:3000/", "tok")
assert client.base_url == "http://gitea.local:3000"
def test_default_timeout_is_30(self):
"""Default timeout is 30 seconds."""
client = GiteaClient("http://gitea.local:3000", "tok")
assert client.timeout == 30
def test_custom_timeout_is_stored(self):
"""Custom timeout is stored and used."""
client = GiteaClient("http://gitea.local:3000", "tok", timeout=10)
assert client.timeout == 10
class TestGetPaginated:
"""Test internal pagination logic."""
def _make_client(self):
return GiteaClient("http://gitea.local:3000", "tok")
def test_single_page(self):
"""When response has fewer items than limit, stop after one request."""
client = self._make_client()
# Return 3 items (< 50 limit) -> single page
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = [{"id": 1}, {"id": 2}, {"id": 3}]
with patch.object(client.session, "get", return_value=mock_resp) as mock_get:
result = client._get_paginated("/api/v1/user/repos")
assert result == [{"id": 1}, {"id": 2}, {"id": 3}]
# Called exactly once (single page)
mock_get.assert_called_once()
def test_two_pages(self):
"""When first page is full (limit items), fetch second page."""
client = self._make_client()
page1 = [{"id": i} for i in range(50)] # Exactly limit=50
page2 = [{"id": i} for i in range(50, 60)] # 10 items -> last page
mock_resp1 = MagicMock()
mock_resp1.raise_for_status = MagicMock()
mock_resp1.json.return_value = page1
mock_resp2 = MagicMock()
mock_resp2.raise_for_status = MagicMock()
mock_resp2.json.return_value = page2
with patch.object(client.session, "get", side_effect=[mock_resp1, mock_resp2]):
result = client._get_paginated("/api/v1/user/repos")
assert len(result) == 60
assert result == page1 + page2
def test_pagination_params_forwarded(self):
"""Extra params are merged with pagination params."""
client = self._make_client()
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = []
with patch.object(client.session, "get", return_value=mock_resp) as mock_get:
client._get_paginated("/api/v1/repos/o/r/milestones", params={"state": "open"})
call_params = mock_get.call_args[1]["params"]
assert call_params["state"] == "open"
assert call_params["limit"] == 50
assert call_params["page"] == 1
class TestGetRepos:
"""Test get_repos method."""
def test_get_repos_calls_paginated(self):
"""get_repos delegates to _get_paginated with correct endpoint."""
client = GiteaClient("http://gitea.local:3000", "tok")
with patch.object(client, "_get_paginated", return_value=[{"id": 1}]) as mock_pag:
result = client.get_repos()
mock_pag.assert_called_once_with("/api/v1/user/repos")
assert result == [{"id": 1}]
class TestGetLatestRelease:
"""Test get_latest_release method."""
def test_returns_release_on_success(self):
"""Returns release dict when repo has a release."""
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"tag_name": "v1.0", "published_at": "2026-01-01"}
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_release("admin", "my-repo")
assert result == {"tag_name": "v1.0", "published_at": "2026-01-01"}
def test_returns_none_on_404(self):
"""Returns None when repo has no release (404)."""
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_release("admin", "no-release-repo")
assert result is None
def test_raises_on_server_error(self):
"""HTTP 500 raises an exception instead of silently returning bad data."""
import requests as req
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.raise_for_status.side_effect = req.HTTPError("500 Server Error")
with patch.object(client.session, "get", return_value=mock_resp):
with pytest.raises(req.HTTPError):
client.get_latest_release("admin", "my-repo")
class TestGetMilestones:
"""Test get_milestones method."""
def test_get_milestones_calls_paginated_with_state_open(self):
"""get_milestones delegates to _get_paginated with state=open."""
client = GiteaClient("http://gitea.local:3000", "tok")
milestones = [{"title": "v2.0", "open_issues": 3, "closed_issues": 2}]
with patch.object(client, "_get_paginated", return_value=milestones) as mock_pag:
result = client.get_milestones("admin", "my-repo")
mock_pag.assert_called_once_with(
"/api/v1/repos/admin/my-repo/milestones",
params={"state": "open"},
)
assert result == milestones
class TestGetWithRetry:
"""Test _get_with_retry method (retry on timeout)."""
def _make_client(self):
return GiteaClient("http://gitea.local:3000", "tok")
@patch("time.sleep")
def test_success_first_attempt(self, mock_sleep):
"""No timeout — returns response directly without sleeping."""
client = self._make_client()
mock_resp = MagicMock()
with patch.object(client.session, "get", return_value=mock_resp):
result = client._get_with_retry("http://gitea.local:3000/api/v1/test")
assert result is mock_resp
mock_sleep.assert_not_called()
@patch("time.sleep")
def test_success_after_timeout(self, mock_sleep):
"""First call times out, second succeeds — one sleep of 1.0s."""
client = self._make_client()
mock_resp = MagicMock()
with patch.object(
client.session, "get", side_effect=[requests.Timeout("timeout"), mock_resp]
):
result = client._get_with_retry("http://gitea.local:3000/api/v1/test")
assert result is mock_resp
mock_sleep.assert_called_once_with(1.0)
@patch("time.sleep")
def test_all_timeouts(self, mock_sleep):
"""All 3 attempts timeout — raises Timeout, sleeps twice (1.0, 2.0)."""
client = self._make_client()
timeout_exc = requests.Timeout("timeout")
with patch.object(
client.session, "get", side_effect=[timeout_exc, timeout_exc, timeout_exc]
):
with pytest.raises(requests.Timeout):
client._get_with_retry("http://gitea.local:3000/api/v1/test")
assert mock_sleep.call_count == 2
mock_sleep.assert_any_call(1.0)
mock_sleep.assert_any_call(2.0)
class TestGetLatestCommit:
"""Test get_latest_commit method."""
def test_returns_first_commit(self):
"""Returns the first commit from the list."""
client = GiteaClient("http://gitea.local:3000", "tok")
commit = {"sha": "abc123", "created": "2026-03-10T14:30:00Z"}
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = [commit]
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_commit("admin", "my-repo")
assert result == commit
def test_empty_repo_returns_none(self):
"""Returns None when repo has no commits."""
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = []
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_commit("admin", "empty-repo")
assert result is None
def test_404_returns_none(self):
"""Returns None when repo is not found (404)."""
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_commit("admin", "missing-repo")
assert result is None