204 lines
7.8 KiB
Python
204 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.gitea import GiteaClient, GiteaError
|
|
from app.store import ScanStore
|
|
|
|
|
|
def _transport(url: str, headers: dict[str, str]) -> tuple[int, bytes]:
|
|
assert headers["Authorization"] == "token test-token"
|
|
if "/user/repos" in url:
|
|
return 200, json.dumps(
|
|
[
|
|
{
|
|
"id": 64,
|
|
"name": "RepoPulse",
|
|
"full_name": "Jens/RepoPulse",
|
|
"owner": {"login": "Jens"},
|
|
"description": "Repository intelligence",
|
|
"default_branch": "main",
|
|
"clone_url": "https://evil.example/Jens/RepoPulse.git",
|
|
"private": True,
|
|
}
|
|
]
|
|
).encode()
|
|
if "/branches/main" in url:
|
|
return 200, b'{"commit":{"id":"0123456789abcdef0123456789abcdef01234567"}}'
|
|
raise AssertionError(url)
|
|
|
|
|
|
def test_gitea_discovery_is_bounded_and_preserves_stable_identity() -> None:
|
|
client = GiteaClient(
|
|
"http://gitea.local",
|
|
"test-token",
|
|
transport=_transport,
|
|
)
|
|
|
|
discovered = client.discover()
|
|
|
|
assert len(discovered) == 1
|
|
repository, revision = discovered[0]
|
|
assert repository.id == "gitea:64"
|
|
assert repository.provider_repository_id == "64"
|
|
assert repository.full_name == "Jens/RepoPulse"
|
|
assert repository.clone_url == "http://gitea.local/Jens/RepoPulse.git"
|
|
assert repository.latest_revision == revision
|
|
|
|
|
|
def test_gitea_auth_errors_are_sanitized() -> None:
|
|
def rejected(_url: str, _headers: dict[str, str]) -> tuple[int, bytes]:
|
|
return 401, b"secret-bearing upstream response"
|
|
|
|
client = GiteaClient("http://gitea.local", "test-token", transport=rejected)
|
|
|
|
try:
|
|
client.list_repositories()
|
|
except GiteaError as exc:
|
|
assert exc.code == "authentication"
|
|
assert "test-token" not in str(exc)
|
|
assert "secret-bearing" not in str(exc)
|
|
else:
|
|
raise AssertionError("authentication error was not raised")
|
|
|
|
|
|
@pytest.mark.parametrize("status_code, error_code", [(401, "authentication"), (403, "authentication"), (404, "not_found"), (429, "rate_limited"), (500, "upstream")])
|
|
def test_gitea_http_statuses_are_classified_without_upstream_body(status_code: int, error_code: str) -> None:
|
|
client = GiteaClient(
|
|
"http://gitea.local",
|
|
"test-token",
|
|
transport=lambda *_: (status_code, b"token=test-token upstream body"),
|
|
)
|
|
|
|
with pytest.raises(GiteaError) as raised:
|
|
client.list_repositories()
|
|
|
|
assert raised.value.code == error_code
|
|
assert "test-token" not in str(raised.value)
|
|
assert "upstream body" not in str(raised.value)
|
|
|
|
|
|
def test_gitea_discovery_paginates_until_a_short_page() -> None:
|
|
calls: list[str] = []
|
|
|
|
def paginated(url: str, headers: dict[str, str]) -> tuple[int, bytes]:
|
|
calls.append(url)
|
|
assert headers["Authorization"] == "token test-token"
|
|
if "/user/repos" in url:
|
|
if "page=1" in url:
|
|
payload = [{"id": 64, "owner": {"login": "Jens"}, "name": "One", "default_branch": "main"}]
|
|
elif "page=2" in url:
|
|
payload = [{"id": 65, "owner": {"login": "Jens"}, "name": "Two", "default_branch": "trunk"}]
|
|
else:
|
|
payload = []
|
|
return 200, json.dumps(payload).encode()
|
|
if "/branches/main" in url:
|
|
return 200, b'{"commit":{"id":"0123456789abcdef0123456789abcdef01234567"}}'
|
|
if "/branches/trunk" in url:
|
|
return 200, b'{"commit":{"sha":"abcdef0123456789abcdef0123456789abcdef01"}}'
|
|
raise AssertionError(url)
|
|
|
|
client = GiteaClient("http://gitea.local", "test-token", page_size=1, max_pages=3, transport=paginated)
|
|
|
|
discovered = client.discover()
|
|
|
|
assert [repository.id for repository, _ in discovered] == ["gitea:64", "gitea:65"]
|
|
assert len([call for call in calls if "/user/repos" in call]) == 3
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"transport, error_code",
|
|
[
|
|
(lambda *_: (200, b"not-json"), "invalid_response"),
|
|
(lambda *_: (_ for _ in ()).throw(TimeoutError()), "unreachable"),
|
|
],
|
|
)
|
|
def test_gitea_malformed_and_timeout_responses_are_safe(transport, error_code: str) -> None:
|
|
client = GiteaClient("http://gitea.local", "test-token", transport=transport)
|
|
|
|
with pytest.raises(GiteaError) as raised:
|
|
client.list_repositories()
|
|
|
|
assert raised.value.code == error_code
|
|
|
|
|
|
def test_gitea_sync_creates_catalog_without_full_reports(tmp_path: Path) -> None:
|
|
store = ScanStore(str(tmp_path / "history.db"))
|
|
client = GiteaClient("http://gitea.local", "test-token", transport=_transport)
|
|
|
|
result = client.sync(store)
|
|
|
|
assert result.discovered == 1
|
|
assert result.created == 1
|
|
repository = store.get_repository("gitea:64")
|
|
assert repository is not None
|
|
assert repository.latest_revision == "0123456789abcdef0123456789abcdef01234567"
|
|
assert repository.description == "Repository intelligence"
|
|
assert store.repository_scans(repository.id) == []
|
|
|
|
|
|
def test_gitea_outage_preserves_catalog_and_marks_sync_error(tmp_path: Path) -> None:
|
|
store = ScanStore(str(tmp_path / "history.db"))
|
|
healthy = GiteaClient("http://gitea.local", "test-token", transport=_transport)
|
|
healthy.sync(store)
|
|
|
|
unavailable = GiteaClient(
|
|
"http://gitea.local",
|
|
"test-token",
|
|
transport=lambda *_: (503, b"upstream details must not escape"),
|
|
)
|
|
result = unavailable.sync(store)
|
|
|
|
assert result.errors == ["upstream: Gitea returned a server error"]
|
|
repository = store.get_repository("gitea:64")
|
|
assert repository is not None
|
|
assert repository.available is True
|
|
assert repository.latest_revision == "0123456789abcdef0123456789abcdef01234567"
|
|
assert repository.sync_error == "upstream: Gitea returned a server error"
|
|
|
|
|
|
def test_complete_sync_marks_missing_repository_unavailable_without_deleting_history(tmp_path: Path) -> None:
|
|
store = ScanStore(str(tmp_path / "history.db"))
|
|
GiteaClient("http://gitea.local", "test-token", transport=_transport).sync(store)
|
|
missing = GiteaClient("http://gitea.local", "test-token", transport=lambda *_: (200, b"[]"))
|
|
|
|
result = missing.sync(store)
|
|
|
|
assert result.unavailable == 1
|
|
repository = store.get_repository("gitea:64")
|
|
assert repository is not None
|
|
assert repository.available is False
|
|
assert store.repository_scans(repository.id) == []
|
|
|
|
|
|
def test_branch_resolution_failure_is_partial_and_preserves_other_catalog_entries(tmp_path: Path) -> None:
|
|
def partial_transport(url: str, _headers: dict[str, str]) -> tuple[int, bytes]:
|
|
if "/user/repos" in url:
|
|
return 200, json.dumps(
|
|
[
|
|
{"id": 1, "owner": {"login": "Jens"}, "name": "Healthy", "default_branch": "main"},
|
|
{"id": 2, "owner": {"login": "Jens"}, "name": "Broken", "default_branch": "main"},
|
|
]
|
|
).encode()
|
|
if "/repos/Jens/Healthy/branches/main" in url:
|
|
return 200, b'{"commit":{"id":"0123456789abcdef0123456789abcdef01234567"}}'
|
|
if "/repos/Jens/Broken/branches/main" in url:
|
|
return 500, b"upstream details must remain private"
|
|
raise AssertionError(url)
|
|
|
|
store = ScanStore(str(tmp_path / "history.db"))
|
|
result = GiteaClient("http://gitea.local", "test-token", transport=partial_transport).sync(store)
|
|
|
|
assert result.discovered == 1
|
|
assert result.partial is True
|
|
assert result.branch_failures == 1
|
|
assert result.listing_complete is True
|
|
assert "Jens/Broken: upstream: Gitea returned a server error" in result.errors
|
|
assert store.get_repository("gitea:1").latest_revision
|
|
broken = store.get_repository("gitea:2")
|
|
assert broken is not None and broken.available is True
|
|
assert broken.sync_error == "Jens/Broken: upstream: Gitea returned a server error"
|