Files
kontextual-engine/tests/cmis/test_cmis_browser_binding_api.py
2026-05-14 02:20:17 +02:00

836 lines
34 KiB
Python

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/root" 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/content-bytes/{object_id}" in paths
assert "/cmis/{access_point_id}/browser/acl/{object_id}" in paths
assert "/cmis/{access_point_id}/browser/parents/{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
assert "/cmis/{access_point_id}/browser/folder" in paths
def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
access_points = cmis_client.get("/cmis").json()
service_document = cmis_client.get("/cmis/readonly-browser/browser").json()
repository = service_document["kontextual-readonly-browser"]
types = cmis_client.get("/cmis/readonly-browser/browser/types").json()
browser_types = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeChildren"},
).json()
browser_types_with_properties = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeChildren", "includePropertyDefinitions": "true"},
).json()
browser_type_descendants = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeDescendants"},
).json()
browser_type_definition = cmis_client.get(
"/cmis/readonly-browser/browser",
params={"cmisselector": "typeDefinition", "typeId": "cmis:document"},
).json()
root_policies = cmis_client.get(
"/cmis/readonly-browser/browser/root",
params={"cmisselector": "policies"},
).json()
root_object = cmis_client.get(
"/cmis/readonly-browser/browser/root",
params={"cmisselector": "object"},
).json()
assert access_points["count"] == 4
assert repository["repositoryId"] == "kontextual-readonly-browser"
assert repository["cmisVersionSupported"] == "1.1"
assert repository["repositoryUrl"].endswith("/cmis/readonly-browser/browser")
assert repository["rootFolderUrl"].endswith("/cmis/readonly-browser/browser/root")
assert repository["capabilities"]["capabilityQuery"] == "metadataonly"
assert repository["capabilities"]["capabilityOrderBy"] == "common"
assert repository["capabilities"]["capabilityGetDescendants"] is False
assert browser_types["types"][0]["id"] == "cmis:document"
assert "propertyDefinitions" not in browser_types["types"][0]
assert "propertyDefinitions" in browser_types_with_properties["types"][0]
assert browser_type_descendants[0]["type"]["id"] == "cmis:document"
assert "propertyDefinitions" not in browser_type_descendants[0]["type"]
assert "propertyDefinitions" in browser_type_definition
assert browser_type_descendants[0]["children"] == []
assert root_policies == []
assert root_object["properties"]["kontextual:filingSource"]["value"] == "root"
assert root_object["properties"]["kontextual:workspaceFolder"]["value"] is False
assert "kontextual:assetId" in browser_type_definition["propertyDefinitions"]
assert browser_type_definition["propertyDefinitions"]["kontextual:topics"]["cardinality"] == "multi"
assert "cmis:path" not in browser_type_definition["propertyDefinitions"]
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:
root_children = cmis_client.get("/cmis/readonly-browser/browser/children").json()
children = cmis_client.get(
"/cmis/readonly-browser/browser/children",
params={"folder_id": "cmis:folder:assets::document"},
).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()
filtered_query = cmis_client.get(
"/cmis/readonly-browser/browser/query",
params={"q": "SELECT * FROM cmis:document WHERE kontextual:topics IN ('cmis') ORDER BY cmis:name ASC"},
).json()
relationships = cmis_client.get(
"/cmis/readonly-browser/browser/relationships",
params={"object_id": "cmis:asset:asset-source"},
).json()
target_relationships = cmis_client.get(
"/cmis/readonly-browser/browser/relationships",
params={"object_id": "cmis:asset:asset-public", "relationshipDirection": "target"},
).json()
changes = cmis_client.get("/cmis/readonly-browser/browser/changes").json()
root_ids = {item["object_id"] for item in root_children["objects"]}
child_ids = {item["object_id"] for item in children["objects"]}
assert "cmis:folder:assets" in root_ids
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 [item["object_id"] for item in filtered_query["results"]] == ["cmis:asset:asset-source"]
assert relationships["count"] == 1
assert relationships["items"][0]["properties"]["cmis:targetId"] == "cmis:asset:asset-public"
assert relationships["items"][0]["properties"]["kontextual:relationshipId"]
assert target_relationships["count"] == 1
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",
params={"folder_id": "cmis:folder:assets::document"},
).json()
admin_denied = cmis_client.get("/cmis/admin-export/browser/children")
admin_allowed = cmis_client.get(
"/cmis/admin-export/browser/children",
params={"folder_id": "cmis:folder:assets::document"},
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 == 405
assert response.json()["exception"] == "notSupported"
assert "SELECT * FROM cmis:document" in response.json()["details"]["supported"]
assert response.json()["details"]["orderable_fields"] == [
"cmis:creationDate",
"cmis:lastModificationDate",
"cmis:name",
"cmis:objectId",
]
descendants = cmis_client.get(
"/cmis/readonly-browser/browser/root",
params={"cmisselector": "descendants"},
)
assert descendants.status_code == 405
assert descendants.json()["exception"] == "notSupported"
assert descendants.json()["details"]["unsupported_feature"] == "get_descendants"
def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) -> None:
created = cmis_client.post(
"/cmis/governed-authoring/browser/document",
json={
"asset_id": "asset-api-authored",
"name": "API Authored",
"content": "# API Authored",
"media_type": "text/markdown",
},
)
updated = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/properties",
json={"properties": {"kontextual:metadata:status": "draft"}},
)
streamed = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/content",
json={"content": "# Updated", "media_type": "text/plain; charset=utf-8"},
)
byte_stream = cmis_client.get(
"/cmis/governed-authoring/browser/content-bytes/cmis:asset:asset-api-authored",
)
byte_range = cmis_client.get(
"/cmis/governed-authoring/browser/content-bytes/cmis:asset:asset-api-authored",
params={"offset": 2, "length": 4},
)
byte_offset_zero = cmis_client.get(
"/cmis/governed-authoring/browser/content-bytes/cmis:asset:asset-api-authored",
params={"offset": 0},
)
deleted = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/delete",
json={},
)
assert created.status_code == 200
assert updated.json()["properties"]["kontextual:metadata:status"] == "draft"
assert streamed.json()["content_stream"]["mime_type"] == "text/plain"
assert byte_stream.content == b"# Updated"
assert byte_stream.headers["content-type"] == "text/plain"
assert byte_stream.headers["etag"].startswith("sha256:")
assert byte_range.content == b"Upda"
assert byte_range.headers["content-length"] == "4"
assert byte_offset_zero.status_code == 200
assert byte_offset_zero.content == b"# Updated"
assert "content-range" not in byte_offset_zero.headers
assert deleted.json()["lifecycle"] == "delete_requested"
def test_cmis_browser_binding_create_document_validates_type_and_secondary_ids(cmis_client) -> None:
invalid = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Invalid Document",
},
)
created = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Secondary Document",
"propertyId[2]": "cmis:secondaryObjectTypeIds",
"propertyValue[2][0]": "kontextual:secondary",
},
files={"content": ("secondary.txt", b"Secondary content", "text/plain")},
)
assert invalid.status_code == 400
assert invalid.json()["exception"] == "invalidArgument"
assert invalid.json()["details"]["type_id"] == "cmis:folder"
assert created.status_code == 200
assert created.json()["properties"]["cmis:secondaryObjectTypeIds"]["value"] == ["kontextual:secondary"]
def test_cmis_browser_binding_document_without_content_streams_empty_compatibility_body(cmis_client) -> None:
folder = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "No Content Folder",
},
).json()
document = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"objectId": folder["properties"]["cmis:objectId"]["value"],
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "No Content Document",
},
).json()
object_id = document["properties"]["cmis:objectId"]["value"]
content = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "content", "objectId": object_id},
)
assert document["properties"]["cmis:contentStreamMimeType"]["value"] is None
assert content.status_code == 200
assert content.content == b""
assert content.headers["content-length"] == "0"
def test_cmis_browser_binding_delete_content_stream_tombstones_content(cmis_client) -> None:
document = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Delete Content Document",
},
files={"content": ("delete-content.txt", b"Delete me", "text/plain")},
).json()
object_id = document["properties"]["cmis:objectId"]["value"]
token = document["properties"]["cmis:changeToken"]["value"]
deleted = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={"cmisaction": "deleteContentStream", "objectId": object_id, "changeToken": token},
)
content_after_delete = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "content", "objectId": object_id},
)
assert deleted.status_code == 200
assert deleted.json()["properties"]["cmis:contentStreamLength"]["value"] is None
assert deleted.json()["properties"]["cmis:changeToken"]["value"] != token
assert content_after_delete.status_code == 409
assert content_after_delete.json()["exception"] == "constraint"
def test_cmis_browser_binding_append_content_stream_adds_versioned_content(cmis_client) -> None:
document = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Append Content Document",
},
files={"content": ("append-content.txt", b"one", "text/plain")},
).json()
object_id = document["properties"]["cmis:objectId"]["value"]
token = document["properties"]["cmis:changeToken"]["value"]
appended = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "appendContent",
"objectId": object_id,
"changeToken": token,
"isLastChunk": "true",
},
files={"content": ("append-content.txt", b" two", "text/plain")},
)
content = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "content", "objectId": object_id},
)
stale_append = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "appendContentStream",
"objectId": object_id,
"changeToken": token,
},
files={"content": ("append-content.txt", b" stale", "text/plain")},
)
assert appended.status_code == 200
assert appended.json()["properties"]["cmis:contentStreamLength"]["value"] == 7
assert appended.json()["properties"]["cmis:contentStreamMimeType"]["value"] == "text/plain"
assert appended.json()["properties"]["cmis:changeToken"]["value"] != token
assert content.content == b"one two"
assert stale_append.status_code == 409
assert stale_append.json()["exception"] == "updateConflict"
def test_cmis_browser_binding_change_tokens_conflict_on_stale_updates(cmis_client) -> None:
document = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Token Document",
},
files={"content": ("token.txt", b"Token content", "text/plain")},
).json()
object_id = document["properties"]["cmis:objectId"]["value"]
original_token = document["properties"]["cmis:changeToken"]["value"]
renamed = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "updateProperties",
"objectId": object_id,
"changeToken": original_token,
"propertyId[0]": "cmis:name",
"propertyValue[0]": "Token Document Renamed",
},
)
renamed_token = renamed.json()["properties"]["cmis:changeToken"]["value"]
stale_property_update = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "updateProperties",
"objectId": object_id,
"changeToken": original_token,
"propertyId[0]": "cmis:description",
"propertyValue[0]": "stale",
},
)
content_updated = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "setContentStream",
"objectId": object_id,
"changeToken": renamed_token,
"content": "Updated token content",
"media_type": "text/plain",
},
)
stale_content_update = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "setContentStream",
"objectId": object_id,
"changeToken": renamed_token,
"content": "Stale update",
"media_type": "text/plain",
},
)
assert renamed.status_code == 200
assert renamed_token != original_token
assert stale_property_update.status_code == 409
assert stale_property_update.json()["exception"] == "updateConflict"
assert content_updated.status_code == 200
assert content_updated.json()["properties"]["cmis:changeToken"]["value"] != renamed_token
assert stale_content_update.status_code == 409
assert stale_content_update.json()["exception"] == "updateConflict"
def test_cmis_browser_binding_create_document_from_source_reuses_content_projection(cmis_client) -> None:
source = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Copy Source",
},
files={"content": ("copy-source.txt", b"Source copy bytes", "text/plain")},
).json()
folder = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Copy Destination",
},
).json()
source_id = source["properties"]["cmis:objectId"]["value"]
folder_id = folder["properties"]["cmis:objectId"]["value"]
copied = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocumentFromSource",
"objectId": folder_id,
"sourceId": source_id,
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Copied Document",
},
)
copied_id = copied.json()["properties"]["cmis:objectId"]["value"]
copied_content = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "content", "objectId": copied_id},
)
assert copied.status_code == 200
assert copied_id != source_id
assert copied.json()["properties"]["cmis:name"]["value"] == "Copied Document"
assert copied.json()["properties"]["cmis:contentStreamLength"]["value"] == 17
assert copied_content.content == b"Source copy bytes"
def test_cmis_browser_binding_bulk_update_renames_documents(cmis_client) -> None:
folder_one = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Bulk Folder One",
},
).json()
folder_two = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Bulk Folder Two",
},
).json()
first = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"objectId": folder_one["properties"]["cmis:objectId"]["value"],
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Bulk One",
},
files={"content": ("bulk-one.txt", b"bulk one", "text/plain")},
).json()
second = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createDocument",
"objectId": folder_two["properties"]["cmis:objectId"]["value"],
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Bulk Two",
},
files={"content": ("bulk-two.txt", b"bulk two", "text/plain")},
).json()
first_id = first["properties"]["cmis:objectId"]["value"]
second_id = second["properties"]["cmis:objectId"]["value"]
response = cmis_client.post(
"/cmis/compat-tck/browser",
data={
"cmisaction": "bulkUpdate",
"objectId[0]": first_id,
"changeToken[0]": first["properties"]["cmis:changeToken"]["value"],
"objectId[1]": second_id,
"changeToken[1]": second["properties"]["cmis:changeToken"]["value"],
"propertyId[0]": "cmis:name",
"propertyValue[0]": "Bulk Renamed",
},
)
first_after = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "objectId": first_id},
).json()
second_after = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "objectId": second_id},
).json()
assert response.status_code == 200
assert {item["id"] for item in response.json()} == {first_id, second_id}
assert all(item["changeToken"] for item in response.json())
assert first_after["properties"]["cmis:name"]["value"] == "Bulk Renamed"
assert second_after["properties"]["cmis:name"]["value"] == "Bulk Renamed"
def test_cmis_browser_binding_create_folder_action_creates_workspace_folder(cmis_client) -> None:
created = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Action Workspace",
},
)
folder = created.json()
folder_id = folder["properties"]["cmis:objectId"]["value"]
root_children = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "children"},
).json()
fetched = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "objectId": folder_id},
).json()
parent = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "parent", "objectId": folder_id},
).json()
document = cmis_client.post(
"/cmis/compat-tck/browser/root",
params={"objectId": folder_id},
data={
"cmisaction": "createDocument",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:document",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Multipart Document",
},
files={"content": ("multipart.txt", b"Multipart content", "text/plain")},
)
document_id = document.json()["properties"]["cmis:objectId"]["value"]
document_path = "/Action Workspace/Multipart Document"
fetched_document_by_path = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "path": document_path},
).json()
fetched_document_by_url_path = cmis_client.get(
"/cmis/compat-tck/browser/root/Action%20Workspace/Multipart%20Document",
params={"cmisselector": "object"},
).json()
updated_document_by_alias = cmis_client.post(
"/cmis/compat-tck/browser/root/Action%20Workspace/Multipart%20Document",
data={
"cmisaction": "update",
"propertyId[0]": "cmis:secondaryObjectTypeIds",
"propertyValue[0][0]": "kontextual:secondary",
},
)
destination = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:objectTypeId",
"propertyValue[0]": "cmis:folder",
"propertyId[1]": "cmis:name",
"propertyValue[1]": "Destination",
},
)
destination_id = destination.json()["properties"]["cmis:objectId"]["value"]
moved_document = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={
"cmisaction": "move",
"objectId": document_id,
"sourceFolderId": folder_id,
"targetFolderId": destination_id,
},
)
moved_path = "/Destination/Multipart Document"
fetched_old_document_path_after_move = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "path": document_path},
)
fetched_moved_document_by_path = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "path": moved_path},
).json()
document_parents = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "parents", "objectId": document_id},
).json()
filtered_document = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={
"cmisselector": "object",
"path": moved_path,
"filter": "cmis:objectId,cmis:name",
"includeAllowableActions": False,
"includeACL": False,
},
).json()
filtered_children = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={
"cmisselector": "children",
"objectId": destination_id,
"filter": "cmis:objectId,cmis:name",
"includeAllowableActions": False,
"includeACL": False,
"includePathSegment": False,
},
).json()
fetched_folder_by_path = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "path": "/Action Workspace"},
).json()
deleted_tree = cmis_client.post(
"/cmis/compat-tck/browser/root",
data={"cmisaction": "deleteTree", "objectId": folder_id},
)
root_children_after_delete = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "children"},
).json()
fetched_after_delete = cmis_client.get(
"/cmis/compat-tck/browser/root",
params={"cmisselector": "object", "objectId": folder_id},
)
deleted = cmis_client.post(
"/cmis/compat-tck/browser",
data={"cmisaction": "delete", "objectId": folder_id},
)
assert created.status_code == 200
assert folder["properties"]["cmis:name"]["value"] == "Action Workspace"
assert folder["properties"]["kontextual:workspaceFolder"]["value"] is True
assert folder["allowableActions"]["canDeleteTree"] is True
assert any(item["object"]["properties"]["cmis:objectId"]["value"] == folder_id for item in root_children["objects"])
assert fetched["properties"]["cmis:path"]["value"] == "/Action Workspace"
assert parent["properties"]["cmis:objectId"]["value"] == "cmis-root"
assert document.status_code == 200
assert document.json()["properties"]["cmis:name"]["value"] == "Multipart Document"
assert document.json()["properties"]["cmis:contentStreamLength"]["value"] == 17
assert document.json()["properties"]["cmis:isLatestVersion"]["value"] is True
assert document.json()["properties"]["cmis:secondaryObjectTypeIds"]["value"] == []
assert document.json()["allowableActions"]["canGetFolderParent"] is False
assert document.json()["allowableActions"]["canMoveObject"] is True
assert document_path == "/Action Workspace/Multipart Document"
assert fetched_document_by_path["properties"]["cmis:name"]["value"] == "Multipart Document"
assert fetched_document_by_url_path["properties"]["cmis:name"]["value"] == "Multipart Document"
assert updated_document_by_alias.status_code == 200
assert updated_document_by_alias.json()["properties"]["cmis:secondaryObjectTypeIds"]["value"] == [
"kontextual:secondary"
]
assert destination.status_code == 200
assert moved_document.status_code == 200
assert moved_path == "/Destination/Multipart Document"
assert fetched_old_document_path_after_move.status_code == 404
assert fetched_moved_document_by_path["properties"]["cmis:name"]["value"] == "Multipart Document"
assert document_parents[0]["object"]["properties"]["cmis:path"]["value"] == "/Destination"
assert document_parents[0]["relativePathSegment"] == "Multipart Document"
assert set(filtered_document["properties"]) == {"cmis:objectId", "cmis:name"}
assert "allowableActions" not in filtered_document
assert "pathSegment" not in filtered_children["objects"][0]
assert set(filtered_children["objects"][0]["object"]["properties"]) == {"cmis:objectId", "cmis:name"}
assert "allowableActions" not in filtered_children["objects"][0]["object"]
assert fetched_folder_by_path["properties"]["cmis:objectId"]["value"] == folder_id
assert deleted_tree.json()["failedToDelete"] == []
assert all(
item["object"]["properties"]["cmis:objectId"]["value"] != folder_id
for item in root_children_after_delete["objects"]
)
assert fetched_after_delete.status_code == 404
assert deleted.status_code == 404
def test_cmis_readonly_route_rejects_mutation(cmis_client) -> None:
response = cmis_client.post(
"/cmis/readonly-browser/browser/document",
json={"asset_id": "asset-api-readonly-denied", "name": "Denied"},
)
folder_response = cmis_client.post(
"/cmis/readonly-browser/browser/root",
data={
"cmisaction": "createFolder",
"propertyId[0]": "cmis:name",
"propertyValue[0]": "Denied Folder",
},
)
assert response.status_code == 403
assert folder_response.status_code == 403
def test_cmis_rejects_unsupported_standard_property_update_with_diagnostics(cmis_client) -> None:
response = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-source/properties",
json={"properties": {"cmis:objectTypeId": "cmis:folder"}},
)
assert response.status_code == 400
assert response.json()["exception"] == "invalidArgument"
assert response.json()["details"]["property"] == "cmis:objectTypeId"
assert response.json()["details"]["supported"] == [
"cmis:name",
"cmis:description",
"cmis:secondaryObjectTypeIds",
"kontextual:metadata:<key>",
]