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"