Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions tests/integration/test_ghe_marketplace_install_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
"""Integration test: marketplace install on ``*.ghe.com`` hosts targets enterprise auth.

Closes the regression-trap gap flagged by the review panel for PR #1292
(closes #1285): the unit tests in ``tests/unit/marketplace/`` cover the
resolver layer directly but stop at the canonical string. This test drives
the full pipeline through to :meth:`AuthResolver.resolve_for_dep` so the
auth-routing contract -- enterprise host, never ``github.com`` fallback --
is machine-verified end-to-end, satisfying the secure-by-default and
governed-by-policy invariants the panel called out (#1304).

Stubs at two seams only:

- ``get_marketplace_by_name`` / ``fetch_or_cache``: skip the marketplace
registry + manifest network I/O. These return ``MarketplaceSource``
registry-config (trust boundary the auth-expert confirmed clean), not
manifest content.
- ``AuthResolver._resolve_token``: skip env/gh-cli/credential-helper I/O so
the test is deterministic and does not depend on the runner having tokens.
The ``host_info`` field on the returned ``AuthContext`` is still real
(built by ``classify_host``) -- that is the routing contract under test.
"""

from __future__ import annotations

from unittest.mock import patch

import pytest

from apm_cli.core.auth import AuthResolver
from apm_cli.marketplace.models import (
MarketplaceManifest,
MarketplacePlugin,
MarketplaceSource,
)
from apm_cli.marketplace.resolver import resolve_marketplace_plugin
from apm_cli.models.dependency.reference import DependencyReference
from apm_cli.utils.github_host import default_host

_GHE_HOST = "corp.ghe.com"
_OWNER = "myorg"
_REPO = "my-marketplace"


def _make_source(host: str) -> MarketplaceSource:
return MarketplaceSource(
name=_REPO,
owner=_OWNER,
repo=_REPO,
host=host,
branch="main",
)


def _make_manifest(plugin: MarketplacePlugin) -> MarketplaceManifest:
return MarketplaceManifest(name=_REPO, plugins=(plugin,), plugin_root="")


def _stub_resolve_token(self, host_info, org):
"""Replacement for ``AuthResolver._resolve_token``.

Returns ``(None, "none", "basic")`` so ``resolve`` builds an ``AuthContext``
deterministically without touching ``gh``, env vars, or the credential
helper. ``host_info`` is the real value from ``classify_host`` -- which is
the routing decision we are asserting on.
"""
return None, "none", "basic"


@pytest.mark.integration
class TestGHEMarketplaceInstallAuthRouting:
"""End-to-end: marketplace install on ``*.ghe.com`` routes AuthResolver at the enterprise host."""

@pytest.fixture(autouse=True)
def _isolate_github_host_env(self, monkeypatch):
"""#1285 explicitly notes ``GITHUB_HOST=corp.ghe.com`` is NOT a viable workaround.

Clear it so the bug-fix path (canonical carries host) is what is actually
tested, not env masking the missing prefix.
"""
monkeypatch.delenv("GITHUB_HOST", raising=False)

@pytest.fixture(autouse=True)
def _stub_token_resolution(self):
with patch.object(AuthResolver, "_resolve_token", _stub_resolve_token):
yield

@pytest.mark.parametrize(
"label,plugin_source,expected_virtual_path",
[
("relative-source", "./plugins/my-plugin", "plugins/my-plugin"),
(
"dict-bare-repo",
{"type": "github", "repo": f"{_OWNER}/{_REPO}", "path": "plugins/my-plugin"},
"plugins/my-plugin",
),
(
"dict-host-qualified-repo",
{
"type": "github",
"repo": f"{_GHE_HOST}/{_OWNER}/{_REPO}",
"path": "plugins/my-plugin",
},
"plugins/my-plugin",
),
],
)
def test_ghe_marketplace_routes_auth_at_enterprise_host(
self, label, plugin_source, expected_virtual_path
):
"""Full chain: resolver -> canonical -> parse -> AuthContext bound to enterprise host."""
plugin = MarketplacePlugin(name="my-plugin", source=plugin_source)
with (
patch(
"apm_cli.marketplace.resolver.get_marketplace_by_name",
return_value=_make_source(_GHE_HOST),
),
patch(
"apm_cli.marketplace.resolver.fetch_or_cache",
return_value=_make_manifest(plugin),
),
):
result = resolve_marketplace_plugin("my-plugin", _REPO)

# Resolver-layer contract: canonical carries the enterprise host.
expected_canonical = f"{_GHE_HOST}/{_OWNER}/{_REPO}/plugins/my-plugin"
assert result.canonical == expected_canonical, f"[{label}] canonical mismatch"

# Parse boundary: re-parsing the canonical recovers the GHE host.
# This is the boundary the install pipeline crosses at
# apm_cli.install.package_resolution.resolve_parsed_dependency_reference
# when marketplace_dep_ref is None (the GitHub-family path).
dep_ref = DependencyReference.parse(result.canonical)
assert dep_ref.host == _GHE_HOST
assert dep_ref.repo_url == f"{_OWNER}/{_REPO}"
assert dep_ref.virtual_path == expected_virtual_path

# Auth-routing contract: AuthResolver targets the enterprise host
# (not github.com fallback). This is the exact bug #1285 reported.
auth = AuthResolver()
ctx = auth.resolve_for_dep(dep_ref)
assert ctx.host_info.host == _GHE_HOST, (
f"[{label}] auth resolved at {ctx.host_info.host!r}, not the GHE host -- "
"this is the silent github.com fallback that #1285 fixed"
)
assert ctx.host_info.kind == "ghe_cloud"

def test_github_com_marketplace_keeps_github_default(self):
"""Regression: ``github.com`` marketplace is unchanged (bare canonical, parse default)."""
plugin = MarketplacePlugin(name="my-plugin", source="./plugins/my-plugin")
with (
patch(
"apm_cli.marketplace.resolver.get_marketplace_by_name",
return_value=_make_source("github.com"),
),
patch(
"apm_cli.marketplace.resolver.fetch_or_cache",
return_value=_make_manifest(plugin),
),
):
result = resolve_marketplace_plugin("my-plugin", _REPO)

assert result.canonical == f"{_OWNER}/{_REPO}/plugins/my-plugin"
dep_ref = DependencyReference.parse(result.canonical)
# default_host() applies because the bare canonical carries no host.
# For github.com marketplaces this is the documented + correct behaviour.
assert (dep_ref.host or default_host()) == "github.com"

auth = AuthResolver()
ctx = auth.resolve_for_dep(dep_ref)
assert ctx.host_info.host == "github.com"
assert ctx.host_info.kind == "github"

def test_cross_repo_locks_known_silent_misroute(self):
"""Regression trap for the cross-repo bug class tracked separately in #1305.

A ``*.ghe.com`` marketplace with a cross-repo dict source bears the same
symptoms as #1285 -- canonical emerges bare, parse defaults to ``github.com``.
This is intentionally out of scope of PR #1292; #1305 tracks the fix
(which belongs in the install-time error handler, not the resolver).
The test locks the current behaviour so the future #1305 fix has an
explicit before/after diff to assert against.
"""
plugin = MarketplacePlugin(
name="cross-repo",
source={
"type": "github",
"repo": "anotherorg/anothertool",
"path": "plugins/x",
},
)
with (
patch(
"apm_cli.marketplace.resolver.get_marketplace_by_name",
return_value=_make_source(_GHE_HOST),
),
patch(
"apm_cli.marketplace.resolver.fetch_or_cache",
return_value=_make_manifest(plugin),
),
):
result = resolve_marketplace_plugin("cross-repo", _REPO)

# Pre-existing behaviour: no host prefix for cross-repo (#1305 to fix).
assert result.canonical == "anotherorg/anothertool/plugins/x"
dep_ref = DependencyReference.parse(result.canonical)
auth = AuthResolver()
ctx = auth.resolve_for_dep(dep_ref)
assert ctx.host_info.host == "github.com", (
"If this assertion fails, the cross-repo silent mis-route bug from "
"#1305 has been fixed -- update this test to reflect the new behaviour."
)
Loading