***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
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:
127
tests/e2e/test_mirror_visibility_smoke.py
Normal file
127
tests/e2e/test_mirror_visibility_smoke.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# tests/e2e/test_mirror_visibility_smoke.py
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestMirrorVisibilityE2ESmoke(unittest.TestCase):
|
||||
"""
|
||||
E2E smoke tests for the new mirror visibility feature.
|
||||
|
||||
We intentionally DO NOT execute provider APIs or require tokens.
|
||||
The tests only verify that:
|
||||
- CLI exposes the new subcommands / flags via --help
|
||||
- Python public API surface is wired and importable
|
||||
|
||||
IMPORTANT:
|
||||
- `python -m pkgmgr.cli` is NOT valid unless pkgmgr/cli/__main__.py exists.
|
||||
- In this repo, `from pkgmgr.cli import main` is the stable entrypoint.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _project_root() -> Path:
|
||||
# tests/e2e/... -> project root is parents[2]
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
def _run(self, args: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env.setdefault("PYTHONUNBUFFERED", "1")
|
||||
|
||||
return subprocess.run(
|
||||
args,
|
||||
cwd=str(self._project_root()),
|
||||
env=env,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def _run_pkgmgr(self, pkgmgr_args: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
"""
|
||||
Run the pkgmgr CLI in a way that works both:
|
||||
- when the console script `pkgmgr` is available on PATH
|
||||
- when only source imports are available
|
||||
|
||||
We prefer the console script if present because it's closest to real E2E.
|
||||
"""
|
||||
exe = shutil.which("pkgmgr")
|
||||
if exe:
|
||||
return self._run([exe, *pkgmgr_args])
|
||||
|
||||
# Fallback to a Python-level entrypoint that exists in your repo:
|
||||
# The stacktrace showed: from pkgmgr.cli import main
|
||||
# We call it with argv simulation.
|
||||
code = r"""
|
||||
import sys
|
||||
from pkgmgr.cli import main
|
||||
|
||||
sys.argv = ["pkgmgr"] + sys.argv[1:]
|
||||
main()
|
||||
"""
|
||||
return self._run([sys.executable, "-c", code, *pkgmgr_args])
|
||||
|
||||
def test_cli_help_lists_visibility_and_provision_public(self) -> None:
|
||||
# `pkgmgr mirror --help` should mention "visibility"
|
||||
p = self._run_pkgmgr(["mirror", "--help"])
|
||||
self.assertEqual(
|
||||
p.returncode,
|
||||
0,
|
||||
msg=f"Expected exit code 0, got {p.returncode}\n\nOutput:\n{p.stdout}",
|
||||
)
|
||||
out_lower = p.stdout.lower()
|
||||
self.assertIn("visibility", out_lower)
|
||||
self.assertIn("provision", out_lower)
|
||||
|
||||
# `pkgmgr mirror provision --help` should show `--public`
|
||||
p = self._run_pkgmgr(["mirror", "provision", "--help"])
|
||||
self.assertEqual(
|
||||
p.returncode,
|
||||
0,
|
||||
msg=f"Expected exit code 0, got {p.returncode}\n\nOutput:\n{p.stdout}",
|
||||
)
|
||||
self.assertIn("--public", p.stdout)
|
||||
|
||||
# `pkgmgr mirror visibility --help` should show choices {private, public}
|
||||
p = self._run_pkgmgr(["mirror", "visibility", "--help"])
|
||||
self.assertEqual(
|
||||
p.returncode,
|
||||
0,
|
||||
msg=f"Expected exit code 0, got {p.returncode}\n\nOutput:\n{p.stdout}",
|
||||
)
|
||||
out_lower = p.stdout.lower()
|
||||
self.assertIn("private", out_lower)
|
||||
self.assertIn("public", out_lower)
|
||||
|
||||
def test_python_api_surface_is_exposed(self) -> None:
|
||||
# Ensure public exports exist and setup_mirrors has ensure_visibility in signature.
|
||||
code = r"""
|
||||
import inspect
|
||||
|
||||
from pkgmgr.actions import mirror as mirror_actions
|
||||
from pkgmgr.core import remote_provisioning as rp
|
||||
|
||||
assert hasattr(mirror_actions, "set_mirror_visibility"), "set_mirror_visibility missing in pkgmgr.actions.mirror"
|
||||
assert hasattr(rp, "set_repo_visibility"), "set_repo_visibility missing in pkgmgr.core.remote_provisioning"
|
||||
|
||||
sig = inspect.signature(mirror_actions.setup_mirrors)
|
||||
assert "ensure_visibility" in sig.parameters, "setup_mirrors missing ensure_visibility parameter"
|
||||
|
||||
print("OK")
|
||||
"""
|
||||
p = self._run([sys.executable, "-c", code])
|
||||
self.assertEqual(
|
||||
p.returncode,
|
||||
0,
|
||||
msg=f"Expected exit code 0, got {p.returncode}\n\nOutput:\n{p.stdout}",
|
||||
)
|
||||
self.assertIn("OK", p.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
322
tests/integration/test_visibility_integration.py
Normal file
322
tests/integration/test_visibility_integration.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# tests/integration/test_visibility_integration.py
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.mirror.setup_cmd import setup_mirrors
|
||||
from pkgmgr.actions.mirror.visibility_cmd import set_mirror_visibility
|
||||
from pkgmgr.core.remote_provisioning.types import RepoSpec
|
||||
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
|
||||
class _FakeRegistry:
|
||||
"""
|
||||
Minimal ProviderRegistry-like object for tests.
|
||||
|
||||
- has .providers for provider-hint selection
|
||||
- has .resolve(host) to pick a provider
|
||||
"""
|
||||
|
||||
def __init__(self, provider: Any) -> None:
|
||||
self.providers = [provider]
|
||||
self._provider = provider
|
||||
|
||||
def resolve(self, host: str) -> Any:
|
||||
return self._provider
|
||||
|
||||
|
||||
class FakeProvider:
|
||||
"""
|
||||
Fake remote provider implementing the visibility API surface.
|
||||
|
||||
Key feature: tolerant host matching, because normalize_provider_host()/URL parsing
|
||||
may drop ports or schemes.
|
||||
"""
|
||||
|
||||
kind = "gitea"
|
||||
|
||||
def __init__(self) -> None:
|
||||
# maps (host, owner, name) -> private(bool)
|
||||
self.privacy: Dict[Tuple[str, str, str], bool] = {}
|
||||
self.calls: List[Tuple[str, Any]] = []
|
||||
|
||||
def can_handle(self, host: str) -> bool:
|
||||
return True
|
||||
|
||||
def _candidate_hosts(self, host: str) -> List[str]:
|
||||
"""
|
||||
Be tolerant against host normalization differences:
|
||||
- may contain scheme (https://...)
|
||||
- may contain port (host:2201)
|
||||
"""
|
||||
h = (host or "").strip()
|
||||
if not h:
|
||||
return [h]
|
||||
|
||||
candidates = [h]
|
||||
|
||||
# strip scheme if present
|
||||
if h.startswith("http://"):
|
||||
candidates.append(h[len("http://") :])
|
||||
if h.startswith("https://"):
|
||||
candidates.append(h[len("https://") :])
|
||||
|
||||
# strip port if present (host:port)
|
||||
for c in list(candidates):
|
||||
if ":" in c:
|
||||
candidates.append(c.split(":", 1)[0])
|
||||
|
||||
# de-dup
|
||||
out: List[str] = []
|
||||
for c in candidates:
|
||||
if c not in out:
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
def repo_exists(self, token: str, spec: RepoSpec) -> bool:
|
||||
self.calls.append(("repo_exists", (token, spec)))
|
||||
for h in self._candidate_hosts(spec.host):
|
||||
if (h, spec.owner, spec.name) in self.privacy:
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_repo(self, token: str, spec: RepoSpec):
|
||||
self.calls.append(("create_repo", (token, spec)))
|
||||
# store under the provided host (as-is)
|
||||
self.privacy[(spec.host, spec.owner, spec.name)] = bool(spec.private)
|
||||
return types.SimpleNamespace(status="created", message="created", url=None)
|
||||
|
||||
def get_repo_private(self, token: str, spec: RepoSpec) -> Optional[bool]:
|
||||
self.calls.append(("get_repo_private", (token, spec)))
|
||||
for h in self._candidate_hosts(spec.host):
|
||||
key = (h, spec.owner, spec.name)
|
||||
if key in self.privacy:
|
||||
return self.privacy[key]
|
||||
return None
|
||||
|
||||
def set_repo_private(self, token: str, spec: RepoSpec, *, private: bool) -> None:
|
||||
self.calls.append(("set_repo_private", (token, spec, private)))
|
||||
# update whichever key exists; else create on spec.host
|
||||
for h in self._candidate_hosts(spec.host):
|
||||
key = (h, spec.owner, spec.name)
|
||||
if key in self.privacy:
|
||||
self.privacy[key] = bool(private)
|
||||
return
|
||||
self.privacy[(spec.host, spec.owner, spec.name)] = bool(private)
|
||||
|
||||
|
||||
def _mk_ctx(*, identifier: str, repo_dir: str, mirrors: Dict[str, str]) -> Any:
|
||||
return types.SimpleNamespace(
|
||||
identifier=identifier,
|
||||
repo_dir=repo_dir,
|
||||
resolved_mirrors=mirrors,
|
||||
)
|
||||
|
||||
|
||||
class TestMirrorVisibilityIntegration(unittest.TestCase):
|
||||
"""
|
||||
Integration tests for:
|
||||
- pkgmgr.actions.mirror.visibility_cmd.set_mirror_visibility
|
||||
- pkgmgr.actions.mirror.setup_cmd.setup_mirrors (ensure_visibility semantics)
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
|
||||
def _repo_dir(self, name: str) -> str:
|
||||
d = os.path.join(self.tmp.name, name)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
@patch("pkgmgr.core.credentials.resolver.TokenResolver.get_token")
|
||||
@patch("pkgmgr.core.remote_provisioning.visibility.ProviderRegistry.default")
|
||||
@patch("pkgmgr.actions.mirror.visibility_cmd.build_context")
|
||||
def test_mirror_visibility_applies_to_all_git_mirrors_updated_and_noop(
|
||||
self,
|
||||
m_build_context,
|
||||
m_registry_default,
|
||||
m_get_token,
|
||||
) -> None:
|
||||
"""
|
||||
Scenario:
|
||||
- repo has two git mirrors
|
||||
- one mirror needs update -> UPDATED
|
||||
- second mirror already desired -> NOOP
|
||||
"""
|
||||
provider = FakeProvider()
|
||||
registry = _FakeRegistry(provider)
|
||||
m_registry_default.return_value = registry
|
||||
|
||||
# Avoid interactive token prompt
|
||||
m_get_token.return_value = types.SimpleNamespace(token="test-token")
|
||||
|
||||
# Seed provider state:
|
||||
# - repo1 currently private=True
|
||||
# - We'll set visibility to public -> should UPDATE
|
||||
provider.privacy[("git.veen.world", "me", "repo1")] = True
|
||||
|
||||
repo = {"id": "repo1", "description": "Repo 1"}
|
||||
repo_dir = self._repo_dir("repo1")
|
||||
|
||||
m_build_context.return_value = _mk_ctx(
|
||||
identifier="repo1",
|
||||
repo_dir=repo_dir,
|
||||
mirrors={
|
||||
"origin": "ssh://git.veen.world:2201/me/repo1.git",
|
||||
"backup": "https://git.veen.world:2201/me/repo1.git",
|
||||
},
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
set_mirror_visibility(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir=self.tmp.name,
|
||||
all_repos=[repo],
|
||||
visibility="public",
|
||||
preview=False,
|
||||
)
|
||||
out = buf.getvalue()
|
||||
|
||||
# We apply to BOTH git mirrors.
|
||||
self.assertIn("[MIRROR VISIBILITY] applying to mirror 'origin':", out)
|
||||
self.assertIn("[MIRROR VISIBILITY] applying to mirror 'backup':", out)
|
||||
|
||||
# After first update, second call will see it already public (NOOP).
|
||||
self.assertIn("[REMOTE VISIBILITY] UPDATED:", out)
|
||||
self.assertIn("[REMOTE VISIBILITY] NOOP:", out)
|
||||
|
||||
# Final state must be public (private=False)
|
||||
self.assertFalse(provider.privacy[("git.veen.world", "me", "repo1")])
|
||||
|
||||
@patch("pkgmgr.core.credentials.resolver.TokenResolver.get_token")
|
||||
@patch("pkgmgr.core.remote_provisioning.visibility.ProviderRegistry.default")
|
||||
@patch("pkgmgr.actions.mirror.visibility_cmd.build_context")
|
||||
@patch("pkgmgr.actions.mirror.visibility_cmd.determine_primary_remote_url")
|
||||
def test_mirror_visibility_fallback_to_primary_when_no_git_mirrors(
|
||||
self,
|
||||
m_determine_primary,
|
||||
m_build_context,
|
||||
m_registry_default,
|
||||
m_get_token,
|
||||
) -> None:
|
||||
"""
|
||||
Scenario:
|
||||
- no git mirrors in MIRRORS config
|
||||
- we fall back to primary URL and apply visibility there
|
||||
"""
|
||||
provider = FakeProvider()
|
||||
registry = _FakeRegistry(provider)
|
||||
m_registry_default.return_value = registry
|
||||
m_get_token.return_value = types.SimpleNamespace(token="test-token")
|
||||
|
||||
# Seed state: currently public (private=False), target private -> UPDATED
|
||||
provider.privacy[("git.veen.world", "me", "repo2")] = False
|
||||
|
||||
repo = {"id": "repo2", "description": "Repo 2"}
|
||||
repo_dir = self._repo_dir("repo2")
|
||||
|
||||
m_build_context.return_value = _mk_ctx(
|
||||
identifier="repo2",
|
||||
repo_dir=repo_dir,
|
||||
mirrors={
|
||||
# non-git mirror entries
|
||||
"pypi": "https://pypi.org/project/example/",
|
||||
},
|
||||
)
|
||||
m_determine_primary.return_value = "ssh://git.veen.world:2201/me/repo2.git"
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
set_mirror_visibility(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir=self.tmp.name,
|
||||
all_repos=[repo],
|
||||
visibility="private",
|
||||
preview=False,
|
||||
)
|
||||
out = buf.getvalue()
|
||||
|
||||
self.assertIn("[MIRROR VISIBILITY] applying to primary:", out)
|
||||
self.assertIn("[REMOTE VISIBILITY] UPDATED:", out)
|
||||
self.assertTrue(provider.privacy[("git.veen.world", "me", "repo2")])
|
||||
|
||||
@patch("pkgmgr.actions.mirror.setup_cmd.probe_remote_reachable_detail")
|
||||
@patch("pkgmgr.actions.mirror.setup_cmd.ensure_remote_repository_for_url")
|
||||
@patch("pkgmgr.core.credentials.resolver.TokenResolver.get_token")
|
||||
@patch("pkgmgr.core.remote_provisioning.visibility.ProviderRegistry.default")
|
||||
@patch("pkgmgr.actions.mirror.setup_cmd.build_context")
|
||||
def test_setup_mirrors_provision_public_enforces_visibility_and_private_default(
|
||||
self,
|
||||
m_build_context,
|
||||
m_registry_default,
|
||||
m_get_token,
|
||||
m_ensure_remote_for_url,
|
||||
m_probe,
|
||||
) -> None:
|
||||
"""
|
||||
Covers the "mirror provision --public" semantics:
|
||||
- setup_mirrors(remote=True, ensure_remote=True, ensure_visibility="public")
|
||||
- ensure_remote_repository_for_url is called with private_default=False
|
||||
- then set_repo_visibility is applied (UPDATED/NOOP depending on current state)
|
||||
- git probing is mocked (no subprocess)
|
||||
"""
|
||||
provider = FakeProvider()
|
||||
registry = _FakeRegistry(provider)
|
||||
m_registry_default.return_value = registry
|
||||
m_get_token.return_value = types.SimpleNamespace(token="test-token")
|
||||
|
||||
# Make git probing always OK (no subprocess calls)
|
||||
m_probe.return_value = (True, "")
|
||||
|
||||
# Seed provider: repo4 currently private=True, target public -> UPDATED
|
||||
provider.privacy[("git.veen.world", "me", "repo4")] = True
|
||||
|
||||
repo = {"id": "repo4", "description": "Repo 4", "private": True}
|
||||
repo_dir = self._repo_dir("repo4")
|
||||
|
||||
m_build_context.return_value = _mk_ctx(
|
||||
identifier="repo4",
|
||||
repo_dir=repo_dir,
|
||||
mirrors={
|
||||
"origin": "ssh://git.veen.world:2201/me/repo4.git",
|
||||
},
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
setup_mirrors(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir=self.tmp.name,
|
||||
all_repos=[repo],
|
||||
preview=False,
|
||||
local=False,
|
||||
remote=True,
|
||||
ensure_remote=True,
|
||||
ensure_visibility="public",
|
||||
)
|
||||
out = buf.getvalue()
|
||||
|
||||
# ensure_remote_repository_for_url called and private_default overridden to False
|
||||
self.assertTrue(m_ensure_remote_for_url.called)
|
||||
_, kwargs = m_ensure_remote_for_url.call_args
|
||||
self.assertIn("private_default", kwargs)
|
||||
self.assertFalse(kwargs["private_default"])
|
||||
|
||||
# Visibility should be enforced
|
||||
self.assertIn("[REMOTE VISIBILITY] UPDATED:", out)
|
||||
self.assertFalse(provider.privacy[("git.veen.world", "me", "repo4")])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
177
tests/unit/pkgmgr/actions/mirror/test_visibility_cmd.py
Normal file
177
tests/unit/pkgmgr/actions/mirror/test_visibility_cmd.py
Normal 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()
|
||||
227
tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py
Normal file
227
tests/unit/pkgmgr/core/remote_provisioning/test_visibility.py
Normal 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()
|
||||
Reference in New Issue
Block a user