diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index c3ffd0a..2fed16f 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `aa72a053975f020a793c88ba18c8d6b93bc9f937`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `8aef76ab42f1ff10ee5a1b3ad94a184120687e47`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 32123fd..36dd3e2 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "aa72a053975f020a793c88ba18c8d6b93bc9f937", + "sourceRevision": "8aef76ab42f1ff10ee5a1b3ad94a184120687e47", "files": [ { "path": ".dockerignore", @@ -1759,8 +1759,8 @@ }, { "path": "connectors/gitea/server.py", - "sha256": "f9358c5d53036ad96d0231c0c2f3ba10f99913fb9a77c21b73c2cf799f97efa1", - "bytes": 228338 + "sha256": "4a07c6171893742d7a25e5cbdaa29eef438fc2d381ae93249a4bb6c1be493421", + "bytes": 228331 }, { "path": "connectors/knowledge/__init__.py", @@ -3189,8 +3189,8 @@ }, { "path": "tests/test_gitea_connector.py", - "sha256": "a6ad1f1eceb51107d9b56ceac65185017c4548ffa5d52ddc1808c5f305dfbcdb", - "bytes": 130782 + "sha256": "4fcd0e2a2aa822ca24958d3dcae2eb11f0c2c223da3fbdcc763631a57734b29f", + "bytes": 135127 }, { "path": "tests/test_gitea_merge_eligibility.py", diff --git a/connectors/gitea/server.py b/connectors/gitea/server.py index 2a29cce..ef21f0b 100644 --- a/connectors/gitea/server.py +++ b/connectors/gitea/server.py @@ -540,7 +540,7 @@ class GiteaConnector: "GITEA_DISCOVERY_LIMIT", "repository discovery exceeded 10000 items", ) - if len(items) < 100: + if not items: break else: raise GiteaError( diff --git a/tests/test_gitea_connector.py b/tests/test_gitea_connector.py index 7344c9d..3ef250f 100644 --- a/tests/test_gitea_connector.py +++ b/tests/test_gitea_connector.py @@ -12,6 +12,7 @@ import unittest from datetime import UTC, datetime from pathlib import Path from unittest.mock import patch +from urllib.parse import parse_qs from uuid import UUID, uuid4 import httpx @@ -142,6 +143,40 @@ class FakeChangesetWorkspace: } +class ClampedDiscoveryServer(FakeGiteaServer): + """Model a Gitea deployment that caps requested discovery pages at fifty.""" + + def __init__(self, *, deny_user_repositories: bool = False) -> None: + super().__init__(deny_user_repositories=deny_user_repositories) + self.discovery_page_requests: list[tuple[str, int, int, int]] = [] + + def _handle(self, request: httpx.Request) -> httpx.Response: + if request.url.path == "/api/v1/repos/search": + query = parse_qs(request.url.query.decode("ascii")) + if "q" not in query: + self.recorded_methods.append(request.method) + self.recorded_authorizations.append(request.headers.get("authorization", "")) + self.recorded_paths.append(request.url.path) + repositories = list(self._repos.values()) + if query.get("private") == ["true"]: + repositories = [item for item in repositories if item.get("private") is True] + return self._paged(repositories, query, request) + return super()._handle(request) + + def _paged( + self, items: list[dict[str, object]], query: dict[str, list[str]], request: httpx.Request + ) -> httpx.Response: + page = max(int(query.get("page", ["1"])[0]), 1) + requested_limit = max(int(query.get("limit", ["100"])[0]), 1) + effective_limit = min(requested_limit, 50) + start = (page - 1) * effective_limit + selected = items[start : start + effective_limit] + self.discovery_page_requests.append( + (request.url.path, page, requested_limit, len(selected)) + ) + return self._json(200, selected, request) + + class GiteaConnectorTests(unittest.TestCase): def test_registry_resolves_an_exact_qualified_name_within_project_scope(self) -> None: instance, _ = connector() @@ -227,9 +262,7 @@ class GiteaConnectorTests(unittest.TestCase): "--cgroup-parent=docker", "--cgroup-parent=untrusted" ), PROJECTBRAIN_BROWSER_VALIDATION_WORKFLOW.replace("--cpus=0.5", "--cpus=0.6"), - PROJECTBRAIN_BROWSER_VALIDATION_WORKFLOW.replace( - "--memory=512m", "--memory=513m" - ), + PROJECTBRAIN_BROWSER_VALIDATION_WORKFLOW.replace("--memory=512m", "--memory=513m"), PROJECTBRAIN_BROWSER_VALIDATION_WORKFLOW.replace( "--pids-limit=128", "--pids-limit=129" ), @@ -1330,7 +1363,7 @@ class GiteaConnectorTests(unittest.TestCase): connector_instance.client.close() def test_repository_create_is_idempotent_and_immediately_registered(self) -> None: - connector_instance, fixture = connector() + connector_instance, fixture = connector(ClampedDiscoveryServer()) try: created = connector_instance.repository_create( context(), @@ -2314,7 +2347,7 @@ class GiteaConnectorTests(unittest.TestCase): def test_account_discovery_adds_future_repositories_without_changing_existing_refs( self, ) -> None: - fixture = FakeGiteaServer() + fixture = ClampedDiscoveryServer() fixture._repos["acme/future"] = { "owner": {"login": "acme"}, "name": "future", @@ -2338,8 +2371,69 @@ class GiteaConnectorTests(unittest.TestCase): finally: connector_instance.client.close() + def test_account_discovery_continues_after_provider_clamps_page_limit(self) -> None: + fixture = ClampedDiscoveryServer() + for index in range(85): + name = f"batch-{index:03}" + fixture._repos[f"acme/{name}"] = { + "owner": {"login": "acme"}, + "name": name, + "full_name": f"acme/{name}", + "private": True, + "default_branch": "main", + } + fixture._repos["acme/workmesh"] = { + "owner": {"login": "acme"}, + "name": "workmesh", + "full_name": "acme/workmesh", + "private": True, + "default_branch": "main", + } + connector_instance, _ = connector(fixture) + connector_instance.discovery_projects = {PROJECT_ID: "hub"} + try: + items = connector_instance.repository_list(context(), page=PageRequest(limit=100))[ + "data" + ]["items"] + self.assertIn("all-workmesh", {item["repositoryRef"] for item in items}) + self.assertEqual( + fixture.discovery_page_requests, + [ + ("/api/v1/repos/search", 1, 100, 50), + ("/api/v1/repos/search", 2, 100, 37), + ("/api/v1/repos/search", 3, 100, 0), + ("/api/v1/user/repos", 1, 100, 50), + ("/api/v1/user/repos", 2, 100, 37), + ("/api/v1/user/repos", 3, 100, 0), + ], + ) + finally: + connector_instance.client.close() + + def test_account_discovery_still_fails_closed_after_one_hundred_full_pages(self) -> None: + fixture = ClampedDiscoveryServer() + for index in range(5_000): + name = f"bounded-{index:04}" + fixture._repos[f"acme/{name}"] = { + "owner": {"login": "acme"}, + "name": name, + "full_name": f"acme/{name}", + "private": True, + "default_branch": "main", + } + connector_instance, _ = connector(fixture) + connector_instance.discovery_projects = {PROJECT_ID: "hub"} + try: + with self.assertRaises(GiteaError) as raised: + connector_instance._refresh_discovered(PROJECT_ID) + self.assertEqual(raised.exception.code, "GITEA_DISCOVERY_LIMIT") + self.assertEqual(len(fixture.discovery_page_requests), 100) + self.assertEqual(fixture.discovery_page_requests[-1][1:], (100, 100, 50)) + finally: + connector_instance.client.close() + def test_account_discovery_keeps_private_results_when_user_repo_list_is_denied(self) -> None: - fixture = FakeGiteaServer(deny_user_repositories=True) + fixture = ClampedDiscoveryServer(deny_user_repositories=True) connector_instance, _ = connector(fixture) connector_instance.discovery_projects = {PROJECT_ID: "hub"} try: