***feat(mirror): add remote repository visibility support***
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / lint-shell (push) Has been cancelled
Mark stable commit / lint-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled

* Add mirror visibility subcommand and provision --public flag
* Implement core visibility API with provider support (GitHub, Gitea)
* Extend provider interface and EnsureStatus
* Add unit, integration and e2e tests for visibility handling

https://chatgpt.com/share/6946a44e-4f48-800f-8124-9c0b9b2b6b04
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-20 14:26:55 +01:00
parent a2138c9985
commit 9802293871
16 changed files with 1323 additions and 10 deletions

View File

@@ -0,0 +1,177 @@
# tests/unit/pkgmgr/actions/mirror/test_visibility_cmd.py
from __future__ import annotations
import io
import unittest
from contextlib import redirect_stdout
from unittest.mock import patch, MagicMock
from pkgmgr.actions.mirror.visibility_cmd import set_mirror_visibility
class TestMirrorVisibilityCmd(unittest.TestCase):
def test_invalid_visibility_raises_value_error(self) -> None:
with self.assertRaises(ValueError):
set_mirror_visibility(
selected_repos=[{"id": "x"}],
repositories_base_dir="/tmp",
all_repos=[],
visibility="nope",
)
@patch("pkgmgr.actions.mirror.visibility_cmd.build_context")
@patch("pkgmgr.actions.mirror.visibility_cmd.determine_primary_remote_url")
def test_no_git_mirrors_and_no_primary_prints_nothing_to_do(
self,
mock_determine_primary: MagicMock,
mock_build_ctx: MagicMock,
) -> None:
ctx = MagicMock()
ctx.identifier = "repo1"
ctx.repo_dir = "/tmp/repo1"
ctx.resolved_mirrors = {"pypi": "https://pypi.org/project/x/"} # non-git
mock_build_ctx.return_value = ctx
mock_determine_primary.return_value = None
buf = io.StringIO()
with redirect_stdout(buf):
set_mirror_visibility(
selected_repos=[{"id": "repo1", "description": "desc"}],
repositories_base_dir="/tmp",
all_repos=[],
visibility="public",
preview=True,
)
out = buf.getvalue()
self.assertIn("[MIRROR VISIBILITY] repo1", out)
self.assertIn("Nothing to do.", out)
@patch("pkgmgr.actions.mirror.visibility_cmd.build_context")
@patch("pkgmgr.actions.mirror.visibility_cmd.determine_primary_remote_url")
@patch("pkgmgr.actions.mirror.visibility_cmd.normalize_provider_host")
@patch("pkgmgr.actions.mirror.visibility_cmd.parse_repo_from_git_url")
@patch("pkgmgr.actions.mirror.visibility_cmd.set_repo_visibility")
def test_applies_to_primary_when_no_git_mirrors(
self,
mock_set_repo_visibility: MagicMock,
mock_parse: MagicMock,
mock_norm: MagicMock,
mock_determine_primary: MagicMock,
mock_build_ctx: MagicMock,
) -> None:
ctx = MagicMock()
ctx.identifier = "repo1"
ctx.repo_dir = "/tmp/repo1"
ctx.resolved_mirrors = {} # no mirrors
mock_build_ctx.return_value = ctx
primary = "ssh://git.veen.world:2201/me/repo1.git"
mock_determine_primary.return_value = primary
mock_parse.return_value = ("git.veen.world:2201", "me", "repo1")
mock_norm.return_value = "git.veen.world:2201"
mock_set_repo_visibility.return_value = MagicMock(
status="skipped", message="Preview"
)
buf = io.StringIO()
with redirect_stdout(buf):
set_mirror_visibility(
selected_repos=[{"id": "repo1", "description": "desc"}],
repositories_base_dir="/tmp",
all_repos=[],
visibility="private",
preview=True,
)
mock_set_repo_visibility.assert_called_once()
_, kwargs = mock_set_repo_visibility.call_args
self.assertEqual(
kwargs["private"], True
) # visibility=private => desired_private=True
out = buf.getvalue()
self.assertIn("applying to primary", out)
@patch("pkgmgr.actions.mirror.visibility_cmd.build_context")
@patch("pkgmgr.actions.mirror.visibility_cmd.normalize_provider_host")
@patch("pkgmgr.actions.mirror.visibility_cmd.parse_repo_from_git_url")
@patch("pkgmgr.actions.mirror.visibility_cmd.set_repo_visibility")
def test_applies_to_all_git_mirrors(
self,
mock_set_repo_visibility: MagicMock,
mock_parse: MagicMock,
mock_norm: MagicMock,
mock_build_ctx: MagicMock,
) -> None:
ctx = MagicMock()
ctx.identifier = "repo1"
ctx.repo_dir = "/tmp/repo1"
ctx.resolved_mirrors = {
"origin": "ssh://git.veen.world:2201/me/repo1.git",
"backup": "git@git.veen.world:me/repo1.git",
"notgit": "https://pypi.org/project/x/",
}
mock_build_ctx.return_value = ctx
# For both URLs, parsing returns same repo
mock_parse.return_value = ("git.veen.world", "me", "repo1")
mock_norm.return_value = "git.veen.world"
mock_set_repo_visibility.return_value = MagicMock(
status="noop", message="Already public"
)
buf = io.StringIO()
with redirect_stdout(buf):
set_mirror_visibility(
selected_repos=[{"id": "repo1", "description": "desc"}],
repositories_base_dir="/tmp",
all_repos=[],
visibility="public",
preview=False,
)
# Should be called for origin + backup (2), but not for notgit
self.assertEqual(mock_set_repo_visibility.call_count, 2)
# Each call should request desired private=False for "public"
for call in mock_set_repo_visibility.call_args_list:
_, kwargs = call
self.assertEqual(kwargs["private"], False)
out = buf.getvalue()
self.assertIn("applying to mirror 'origin'", out)
self.assertIn("applying to mirror 'backup'", out)
@patch("pkgmgr.actions.mirror.visibility_cmd.build_context")
@patch("pkgmgr.actions.mirror.visibility_cmd.determine_primary_remote_url")
def test_primary_not_git_prints_nothing_to_do(
self,
mock_determine_primary: MagicMock,
mock_build_ctx: MagicMock,
) -> None:
ctx = MagicMock()
ctx.identifier = "repo1"
ctx.repo_dir = "/tmp/repo1"
ctx.resolved_mirrors = {}
mock_build_ctx.return_value = ctx
mock_determine_primary.return_value = "https://example.com/not-a-git-url"
buf = io.StringIO()
with redirect_stdout(buf):
set_mirror_visibility(
selected_repos=[{"id": "repo1"}],
repositories_base_dir="/tmp",
all_repos=[],
visibility="public",
)
out = buf.getvalue()
self.assertIn("Nothing to do.", out)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,227 @@
# tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py
from __future__ import annotations
import unittest
from unittest.mock import MagicMock
from pkgmgr.core.remote_provisioning.types import (
AuthError,
NetworkError,
PermissionError,
ProviderHint,
RepoSpec,
UnsupportedProviderError,
)
from pkgmgr.core.remote_provisioning.visibility import (
VisibilityOptions,
set_repo_visibility,
)
from pkgmgr.core.remote_provisioning.http.errors import HttpError
class TestSetRepoVisibility(unittest.TestCase):
def _mk_provider(self, *, kind: str = "gitea") -> MagicMock:
p = MagicMock()
p.kind = kind
return p
def _mk_registry(
self, provider: MagicMock | None, providers: list[MagicMock] | None = None
) -> MagicMock:
reg = MagicMock()
reg.resolve.return_value = provider
reg.providers = (
providers
if providers is not None
else ([provider] if provider is not None else [])
)
return reg
def _mk_token_resolver(self, token: str = "TOKEN") -> MagicMock:
resolver = MagicMock()
tok = MagicMock()
tok.token = token
resolver.get_token.return_value = tok
return resolver
def test_preview_returns_skipped_and_does_not_call_provider(self) -> None:
provider = self._mk_provider()
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
res = set_repo_visibility(
spec,
private=False,
options=VisibilityOptions(preview=True),
registry=reg,
token_resolver=resolver,
)
self.assertEqual(res.status, "skipped")
provider.get_repo_private.assert_not_called()
provider.set_repo_private.assert_not_called()
def test_unsupported_provider_raises(self) -> None:
reg = self._mk_registry(provider=None, providers=[])
spec = RepoSpec(host="unknown.host", owner="me", name="repo", private=True)
with self.assertRaises(UnsupportedProviderError):
set_repo_visibility(
spec,
private=True,
registry=reg,
token_resolver=self._mk_token_resolver(),
)
def test_notfound_when_provider_returns_none(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.return_value = None
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
res = set_repo_visibility(
spec,
private=True,
registry=reg,
token_resolver=resolver,
)
self.assertEqual(res.status, "notfound")
provider.set_repo_private.assert_not_called()
def test_noop_when_already_desired(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.return_value = True
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
res = set_repo_visibility(
spec,
private=True,
registry=reg,
token_resolver=resolver,
)
self.assertEqual(res.status, "noop")
provider.set_repo_private.assert_not_called()
def test_updated_when_needs_change(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.return_value = True
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
res = set_repo_visibility(
spec,
private=False,
registry=reg,
token_resolver=resolver,
)
self.assertEqual(res.status, "updated")
provider.set_repo_private.assert_called_once()
args, kwargs = provider.set_repo_private.call_args
self.assertEqual(kwargs.get("private"), False)
def test_provider_hint_overrides_registry_resolution(self) -> None:
# registry.resolve returns gitea provider, but hint forces github provider
gitea = self._mk_provider(kind="gitea")
github = self._mk_provider(kind="github")
github.get_repo_private.return_value = True
reg = self._mk_registry(gitea, providers=[gitea, github])
resolver = self._mk_token_resolver()
spec = RepoSpec(host="github.com", owner="me", name="repo", private=True)
res = set_repo_visibility(
spec,
private=False,
provider_hint=ProviderHint(kind="github"),
registry=reg,
token_resolver=resolver,
)
self.assertEqual(res.status, "updated")
github.get_repo_private.assert_called_once()
gitea.get_repo_private.assert_not_called()
def test_http_error_401_maps_to_auth_error(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.side_effect = HttpError(
status=401, message="nope", body=""
)
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
with self.assertRaises(AuthError):
set_repo_visibility(
spec, private=True, registry=reg, token_resolver=resolver
)
def test_http_error_403_maps_to_permission_error(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.side_effect = HttpError(
status=403, message="nope", body=""
)
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
with self.assertRaises(PermissionError):
set_repo_visibility(
spec, private=True, registry=reg, token_resolver=resolver
)
def test_http_error_status_0_maps_to_network_error(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.side_effect = HttpError(
status=0, message="connection failed", body=""
)
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
with self.assertRaises(NetworkError):
set_repo_visibility(
spec, private=True, registry=reg, token_resolver=resolver
)
def test_http_error_other_maps_to_network_error(self) -> None:
provider = self._mk_provider()
provider.get_repo_private.side_effect = HttpError(
status=500, message="boom", body="server error"
)
reg = self._mk_registry(provider)
resolver = self._mk_token_resolver()
spec = RepoSpec(host="git.veen.world", owner="me", name="repo", private=True)
with self.assertRaises(NetworkError):
set_repo_visibility(
spec, private=True, registry=reg, token_resolver=resolver
)
if __name__ == "__main__":
unittest.main()