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>
116 lines
4.3 KiB
Python
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()
|