Files
mcp-nextcloud/tests/contract/test_astrolabe_capabilities_consumer.py
T
Chris CoutinhoandClaude Opus 4.8 7b43cc8220 docs(contract): clarify minimal OCS envelope + reuse _BROKER_READY
Address round-1 review nits:
- document why _ocs_capabilities omits the rest of the OCS envelope (Pact V4
  allows extra provider-side keys; pin only astrolabe's own block)
- use the module-level _BROKER_READY in the broker-source guard instead of
  re-checking the three env vars inline

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:52:31 +02:00

116 lines
4.3 KiB
Python

"""Consumer contract: nextcloud-mcp-server -> astrolabe OCS capabilities.
The MCP server reads which content sources an admin has approved for semantic
search via :func:`nextcloud_mcp_server.capabilities.allowed_doc_types`, which
calls :meth:`NextcloudClient.capabilities` →
``GET /ocs/v2.php/cloud/capabilities`` and parses
``ocs.data.capabilities.astrolabe.semantic_search.enabled_doc_types`` (the
``OCA\\Astrolabe\\Capabilities`` provider on the astrolabe side).
This pact pins the request shape and the two states the consumer branches on:
- some sources approved -> ``enabled_doc_types`` is a non-empty list, parsed to
the corresponding frozenset (the search/scan/purge gates restrict to it)
- every source disabled -> ``enabled_doc_types`` is ``[]``, parsed to an empty
frozenset (an all-disabled admin config — distinct from "no restriction")
The "no astrolabe block" / fail-open (``None``) case is internal defensive
handling for older astrolabe versions, not a contract obligation of the current
provider, so it is covered by a unit test (``tests/unit/test_capabilities.py``)
rather than a pact interaction.
Only the capability block astrolabe owns is pinned — the real OCS response also
carries many other apps' capabilities, which pact ignores (unspecified keys are
allowed in the provider response).
See ADR-029 for the overall contract-testing architecture.
"""
import pytest
from httpx import BasicAuth
from nextcloud_mcp_server.capabilities import allowed_doc_types, clear_cache
from nextcloud_mcp_server.client import NextcloudClient
pytestmark = pytest.mark.contract
def _ocs_capabilities(enabled_doc_types: list[str]) -> dict:
"""Minimal OCS envelope carrying just astrolabe's semantic_search block.
Intentionally omits the rest of a real OCS response (other apps'
capabilities, ``meta.statuscode``/``message``, etc.): Pact V4 allows extra
provider-side keys, so pinning only the block this consumer reads keeps the
contract focused on what astrolabe owns without coupling to Nextcloud-core
envelope fields.
"""
return {
"ocs": {
"meta": {"status": "ok"},
"data": {
"capabilities": {
"astrolabe": {
"semantic_search": {
"enabled_doc_types": enabled_doc_types,
}
}
}
},
}
}
async def test_capabilities_report_admin_approved_doc_types(consumer_pact):
"""Approved sources are returned and parsed into the allow-set."""
clear_cache()
(
consumer_pact.upon_receiving("a request for OCS capabilities")
.given("astrolabe has approved file and note for semantic search")
.with_request("GET", "/ocs/v2.php/cloud/capabilities")
.with_header("OCS-APIRequest", "true")
.will_respond_with(200)
.with_body(
_ocs_capabilities(["file", "note"]),
content_type="application/json",
)
)
with consumer_pact.serve() as srv:
client = NextcloudClient(
base_url=str(srv.url),
username="admin",
auth=BasicAuth("admin", "app-password"),
)
allowed = await allowed_doc_types(client, "admin")
assert allowed == frozenset({"file", "note"})
async def test_capabilities_report_all_sources_disabled(consumer_pact):
"""An empty allow-set (admin disabled everything) is distinct from None."""
clear_cache()
(
consumer_pact.upon_receiving(
"a request for OCS capabilities with every source disabled"
)
.given("astrolabe has disabled all sources for semantic search")
.with_request("GET", "/ocs/v2.php/cloud/capabilities")
.with_header("OCS-APIRequest", "true")
.will_respond_with(200)
.with_body(
_ocs_capabilities([]),
content_type="application/json",
)
)
with consumer_pact.serve() as srv:
client = NextcloudClient(
base_url=str(srv.url),
username="admin",
auth=BasicAuth("admin", "app-password"),
)
allowed = await allowed_doc_types(client, "admin")
# Present-but-empty => empty frozenset (restrict everything), NOT None.
assert allowed == frozenset()