Files
computer-playbook/tests/unit/python/plugins/lookup/test_mcp_client_bearer.py
T
kevinveenbirkenbachandClaude Opus 5 791eee7c9f refactor(mcp)!: prove the endpoint contract with a CLI test, not a deploy probe
The contract check ran as an Ansible task in the middle of every provider deploy, and the two client checks ran the same way. A test that is part of the deploy cannot be run without deploying, cannot be re-run on its own, and reports its verdict as a deploy failure. Twenty-seven task files lose 491 lines to this.

It is now what it always was. Each provider ships files/test/test.sh, templates/test.env.j2 and meta/tests.yml, and test-e2e-cli stages one shared harness beside it: run.sh drives contract.sh in a throwaway container on the provider's own network, then isolation.sh for adapter-fronted providers. The read probe each provider offers moved from the calling task into meta/mcp.yml next to the allowlist it must satisfy, and the schema now rejects a read probe naming a tool outside that allowlist.

The bearer the test presents is the one a client resolves through mcp.credential, via the new mcp_client_bearer lookup over resolve_credential, which moved to utils.roles.applications.mcp so it is the same function mcp_servers renders every client connection with. The probe presented whatever the calling role passed, which for the fifteen adapter-fronted providers is a different secret than any client holds.

Sidecar isolation is read from the engine rather than measured inside the container, so it no longer needs a delegation to the node holding the task: attachment is compared by network name, which drops the gwbridge special case, and a swarm worker says it cannot measure instead of skipping silently.

Retrying is bounded by wall clock against MCP_DEADLINE_SECONDS, which scales through the same lookup('timeout') as the harness budget it has to stay under. A flag that is neither true nor false aborts rather than reading as off.

mcp-provider-probe becomes mcp-provider-cli-test: an enabled provider must ship the test rather than include the task. mcp-probe-bearer becomes mcp-adapter-bearer, tying ADAPTER_BEARER to the declared credential key now that no probe include carries it. A new integration test renders the shared env template for every provider through Ansible's own templar, so a later change to it breaks visibly instead of poisoning twenty deploys quietly.

Hermes and OpenClaw keep their client-side proof as CLI tests of their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 20:46:26 +02:00

111 lines
3.4 KiB
Python

"""Unit tests for ``plugins/lookup/mcp_client_bearer.py``.
The sub-lookups are stubbed so the tests stay hermetic: what matters is that
the source declared in ``mcp.credential`` decides which secret comes back, and
that a provider fronted by an adapter never answers with the token its upstream
principal holds.
"""
from __future__ import annotations
import importlib.util
import unittest
from unittest import mock
from unittest.mock import patch
from ansible.errors import AnsibleError
from . import PROJECT_ROOT
def _load_module():
path = PROJECT_ROOT / "plugins/lookup/mcp_client_bearer.py"
spec = importlib.util.spec_from_file_location("lookup_mcp_client_bearer", str(path))
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
class _DummyTemplar:
def __init__(self):
self.available_variables = {}
class TestMcpClientBearerLookup(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mod = _load_module()
def _run(self, terms, credential, role_credentials=None, users=None):
lookup = self.mod.LookupModule()
lookup._templar = _DummyTemplar()
lookup._loader = mock.MagicMock()
class _StubConfig:
def run(self, terms_, variables=None, **kwargs):
if terms_[1] == "mcp.credential":
return [credential]
return [role_credentials if role_credentials is not None else {}]
class _StubUsers:
def run(self, terms_, variables=None, **kwargs):
return [users if users is not None else {}]
def _dispatch(name, **_kwargs):
return _StubConfig() if name == "config" else _StubUsers()
with patch.object(self.mod.lookup_loader, "get", side_effect=_dispatch):
return lookup.run(terms, variables={})
def test_an_adapter_answers_with_its_own_rendered_bearer(self):
credential = {
"owner": "mcp-web-app-gitea",
"source": "credentials",
"key": "mcp_bearer",
}
self.assertEqual(
["adapter-secret"],
self._run(
["web-app-gitea"],
credential,
role_credentials={"mcp_bearer": "adapter-secret"},
users={"tokens": {"web-app-gitea": "upstream-pat"}},
),
)
def test_a_native_provider_answers_with_the_stored_token(self):
credential = {
"owner": "mcp-web-app-nextcloud",
"source": "token_store",
"key": "web-app-nextcloud",
}
self.assertEqual(
["stored-token"],
self._run(
["web-app-nextcloud"],
credential,
users={"tokens": {"web-app-nextcloud": "stored-token"}},
),
)
def test_a_provider_without_a_secret_yet_reads_empty(self):
credential = {
"owner": "mcp-web-app-gitea",
"source": "credentials",
"key": "mcp_bearer",
}
self.assertEqual([""], self._run(["web-app-gitea"], credential))
def test_an_undeclared_credential_raises(self):
with self.assertRaises(AnsibleError):
self._run(["web-app-gitea"], None)
def test_a_missing_application_id_raises(self):
with self.assertRaises(AnsibleError):
self._run([], {})
if __name__ == "__main__":
unittest.main()