Implemented profile-scoped CMIS Browser Binding routes

This commit is contained in:
2026-05-07 01:12:38 +02:00
parent 41da61896b
commit 7e168e93d3
6 changed files with 650 additions and 1 deletions

View File

@@ -0,0 +1,164 @@
from __future__ import annotations
import pytest
from kontextual_engine import (
AssetRepresentation,
Classification,
RepresentationKind,
ServiceRuntime,
Sensitivity,
create_app,
)
from kontextual_engine.adapters.memory import InMemoryAssetRegistryRepository
pytestmark = pytest.mark.cmis
@pytest.fixture
def cmis_client():
pytest.importorskip("fastapi")
pytest.importorskip("httpx")
from fastapi.testclient import TestClient
runtime = ServiceRuntime(repository=InMemoryAssetRegistryRepository())
context = runtime.operation_context(actor_id="cmis-test", correlation_id="corr-cmis-api")
runtime.asset_service().create_asset(
"Source",
Classification(
asset_type="document",
sensitivity=Sensitivity.INTERNAL,
owner="Platform Knowledge",
topics=("cmis",),
),
context,
asset_id="asset-source",
representations=[
AssetRepresentation.from_content(
"asset-source",
RepresentationKind.SOURCE,
"text/markdown",
"# Source\n\nCMIS Browser Binding test fixture.",
storage_ref="memory://asset-source/source",
)
],
)
runtime.create_asset(
{
"asset_id": "asset-public",
"title": "Public Target",
"classification": {"asset_type": "document", "sensitivity": "public"},
},
context,
)
runtime.create_asset(
{
"asset_id": "asset-confidential",
"title": "Confidential Target",
"classification": {"asset_type": "document", "sensitivity": "confidential"},
},
context,
)
runtime.create_relationship(
{
"source_asset_id": "asset-source",
"target_id": "asset-public",
"predicate": "references",
"target_kind": "asset",
"confidence": 1.0,
},
context,
)
with TestClient(create_app(runtime)) as test_client:
yield test_client
def test_cmis_browser_binding_routes_are_advertised_in_openapi(cmis_client) -> None:
paths = cmis_client.get("/openapi.json").json()["paths"]
assert "/cmis" in paths
assert "/cmis/{access_point_id}/browser" in paths
assert "/cmis/{access_point_id}/browser/types" in paths
assert "/cmis/{access_point_id}/browser/children" in paths
assert "/cmis/{access_point_id}/browser/object/{object_id}" in paths
assert "/cmis/{access_point_id}/browser/content/{object_id}" in paths
assert "/cmis/{access_point_id}/browser/query" in paths
assert "/cmis/{access_point_id}/browser/relationships" in paths
assert "/cmis/{access_point_id}/browser/changes" in paths
def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
access_points = cmis_client.get("/cmis").json()
repository = cmis_client.get("/cmis/readonly-browser/browser").json()
types = cmis_client.get("/cmis/readonly-browser/browser/types").json()
assert access_points["count"] == 4
assert repository["repository_id"] == "kontextual-readonly-browser"
assert repository["cmis_version_supported"] == "1.1"
assert repository["capabilities"]["capability_query"] == "metadataonly"
assert {item["base_type_id"] for item in types["items"]} >= {
"cmis:document",
"cmis:folder",
"cmis:relationship",
}
def test_cmis_readonly_children_object_content_query_relationships_and_changes(cmis_client) -> None:
children = cmis_client.get("/cmis/readonly-browser/browser/children").json()
object_response = cmis_client.get(
"/cmis/readonly-browser/browser/object/cmis:asset:asset-source"
).json()
content = cmis_client.get(
"/cmis/readonly-browser/browser/content/cmis:asset:asset-source"
).json()
query = cmis_client.get(
"/cmis/readonly-browser/browser/query",
params={"q": "SELECT * FROM cmis:document"},
).json()
relationships = cmis_client.get(
"/cmis/readonly-browser/browser/relationships",
params={"object_id": "cmis:asset:asset-source"},
).json()
changes = cmis_client.get("/cmis/readonly-browser/browser/changes").json()
child_ids = {item["object_id"] for item in children["objects"]}
assert "cmis:asset:asset-source" in child_ids
assert "cmis:asset:asset-public" in child_ids
assert "cmis:asset:asset-confidential" not in child_ids
assert object_response["properties"]["kontextual:assetId"] == "asset-source"
assert "get_content_stream" in object_response["allowable_actions"]
assert content["mime_type"] == "text/markdown"
assert query["total_num_items"] == children["total_num_items"]
assert relationships["count"] == 1
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-public"
assert changes["total_num_items"] >= 3
def test_cmis_profile_gates_visibility_by_access_point(cmis_client) -> None:
readonly = cmis_client.get("/cmis/readonly-browser/browser/children").json()
admin_denied = cmis_client.get("/cmis/admin-export/browser/children")
admin_allowed = cmis_client.get(
"/cmis/admin-export/browser/children",
headers={"X-Actor-Type": "service_account", "X-Actor-Id": "svc-export"},
).json()
readonly_ids = {item["object_id"] for item in readonly["objects"]}
admin_ids = {item["object_id"] for item in admin_allowed["objects"]}
assert admin_denied.status_code == 403
assert "cmis:asset:asset-confidential" not in readonly_ids
assert "cmis:asset:asset-confidential" in admin_ids
def test_cmis_query_reports_unsupported_subset_diagnostics(cmis_client) -> None:
response = cmis_client.get(
"/cmis/readonly-browser/browser/query",
params={"q": "SELECT * FROM cmis:document JOIN cmis:relationship"},
)
assert response.status_code == 422
assert response.json()["detail"]["details"]["supported"] == [
"SELECT * FROM cmis:document",
"SELECT * FROM kontextual:document",
]

View File

@@ -0,0 +1,120 @@
from __future__ import annotations
import pytest
from kontextual_engine import (
AssetRepresentation,
Classification,
RepresentationKind,
ServiceRuntime,
Sensitivity,
)
from kontextual_engine.adapters.memory import InMemoryAssetRegistryRepository
pytestmark = pytest.mark.cmis
@pytest.fixture
def cmis_runtime() -> tuple[ServiceRuntime, object]:
runtime = ServiceRuntime(repository=InMemoryAssetRegistryRepository())
context = runtime.operation_context(actor_id="cmis-runtime", correlation_id="corr-cmis-runtime")
runtime.asset_service().create_asset(
"Runtime Source",
Classification(
asset_type="document",
sensitivity=Sensitivity.INTERNAL,
owner="Platform Knowledge",
topics=("cmis",),
),
context,
asset_id="asset-runtime-source",
representations=[
AssetRepresentation.from_content(
"asset-runtime-source",
RepresentationKind.SOURCE,
"text/markdown",
"# Runtime Source\n\nCMIS runtime fixture.",
storage_ref="memory://asset-runtime-source/source",
)
],
)
runtime.create_asset(
{
"asset_id": "asset-runtime-public",
"title": "Runtime Public",
"classification": {"asset_type": "document", "sensitivity": "public"},
},
context,
)
runtime.create_asset(
{
"asset_id": "asset-runtime-confidential",
"title": "Runtime Confidential",
"classification": {"asset_type": "document", "sensitivity": "confidential"},
},
context,
)
runtime.create_relationship(
{
"source_asset_id": "asset-runtime-source",
"target_id": "asset-runtime-public",
"predicate": "references",
"target_kind": "asset",
"confidence": 0.99,
},
context,
)
return runtime, context
def test_runtime_cmis_browser_repository_types_children_and_object(cmis_runtime) -> None:
runtime, context = cmis_runtime
access_points = runtime.cmis_access_points()
repository = runtime.cmis_repository_info("readonly-browser")
types = runtime.cmis_type_definitions("readonly-browser")
children = runtime.cmis_children("readonly-browser", context)
obj = runtime.cmis_object("readonly-browser", "cmis:asset:asset-runtime-source", context)
assert access_points["count"] == 4
assert repository["repository_id"] == "kontextual-readonly-browser"
assert repository["capabilities"]["capability_get_descendants"] is True
assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"}
object_ids = {item["object_id"] for item in children["objects"]}
assert "cmis:asset:asset-runtime-source" in object_ids
assert "cmis:asset:asset-runtime-public" in object_ids
assert "cmis:asset:asset-runtime-confidential" not in object_ids
assert obj["properties"]["kontextual:assetId"] == "asset-runtime-source"
def test_runtime_cmis_browser_content_query_relationships_and_changes(cmis_runtime) -> None:
runtime, context = cmis_runtime
content = runtime.cmis_content_stream("readonly-browser", "cmis:asset:asset-runtime-source", context)
query = runtime.cmis_query("readonly-browser", "SELECT * FROM cmis:document", context)
relationships = runtime.cmis_relationships(
"readonly-browser",
context,
object_id="cmis:asset:asset-runtime-source",
)
changes = runtime.cmis_change_log("readonly-browser", context)
assert content["mime_type"] in {"text/plain", "text/markdown"}
assert query["total_num_items"] == 2
assert relationships["count"] == 1
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-runtime-public"
assert changes["total_num_items"] >= 3
def test_runtime_cmis_browser_rejects_unsupported_query_subset(cmis_runtime) -> None:
runtime, context = cmis_runtime
with pytest.raises(Exception) as exc_info:
runtime.cmis_query(
"readonly-browser",
"SELECT * FROM cmis:document JOIN cmis:relationship",
context,
)
assert "Unsupported CMIS query subset" in str(exc_info.value)

View File

@@ -654,6 +654,9 @@ def test_service_health_readiness_version_and_openapi_contracts(client) -> None:
assert "/api/v1/ready" in paths
assert "/api/v1/version" in paths
assert "/api/v1/context" in paths
assert "/cmis" in paths
assert "/cmis/{access_point_id}/browser" in paths
assert "/cmis/{access_point_id}/browser/children" in paths
assert "/api/v1/assets" in paths
assert "/api/v1/relationships" in paths
assert "/api/v1/audit/events" in paths