CMIS Browser Binding fixes

This commit is contained in:
2026-05-11 12:28:36 +02:00
parent 59aa2a49a8
commit dc32be36fb
8 changed files with 1422 additions and 121 deletions

View File

@@ -142,7 +142,7 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
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" in browser_type_definition["propertyDefinitions"]
assert "cmis:path" not in browser_type_definition["propertyDefinitions"]
assert {item["base_type_id"] for item in types["items"]} >= {
"cmis:document",
"cmis:folder",
@@ -236,11 +236,15 @@ def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) ->
)
streamed = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/content",
json={"content": "# Updated", "media_type": "text/markdown"},
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},
)
deleted = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-api-authored/delete",
json={},
@@ -248,12 +252,80 @@ def test_cmis_governed_authoring_routes_allow_selected_mutations(cmis_client) ->
assert created.status_code == 200
assert updated.json()["properties"]["kontextual:metadata:status"] == "draft"
assert streamed.json()["content_stream"]["mime_type"] == "text/markdown"
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 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 == 422
assert invalid.json()["detail"]["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_create_folder_action_creates_workspace_folder(cmis_client) -> None:
created = cmis_client.post(
"/cmis/compat-tck/browser/root",
@@ -291,14 +363,77 @@ def test_cmis_browser_binding_create_folder_action_creates_workspace_folder(cmis
},
files={"content": ("multipart.txt", b"Multipart content", "text/plain")},
)
document_path = document.json()["properties"]["cmis:path"]["value"]
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.json()["properties"]["cmis:objectId"]["value"]},
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",
@@ -334,9 +469,26 @@ def test_cmis_browser_binding_create_folder_action_creates_workspace_folder(cmis
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 document_parents[0]["object"]["properties"]["cmis:path"]["value"] == "/Action Workspace"
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(
@@ -368,9 +520,14 @@ def test_cmis_readonly_route_rejects_mutation(cmis_client) -> None:
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:name": "Renamed"}},
json={"properties": {"cmis:objectTypeId": "cmis:folder"}},
)
assert response.status_code == 422
assert response.json()["detail"]["details"]["property"] == "cmis:name"
assert response.json()["detail"]["details"]["supported"] == ["kontextual:metadata:<key>"]
assert response.json()["detail"]["details"]["property"] == "cmis:objectTypeId"
assert response.json()["detail"]["details"]["supported"] == [
"cmis:name",
"cmis:description",
"cmis:secondaryObjectTypeIds",
"kontextual:metadata:<key>",
]

View File

@@ -215,6 +215,24 @@ def test_runtime_cmis_compat_profile_supports_workspace_folder_lifecycle(cmis_ru
folder_children = runtime.cmis_children("compat-tck", context, folder_id=folder_object_id)
document_by_path = runtime.cmis_object_by_path("compat-tck", "/TCK Workspace/Workspace Document", context)
document_parents = runtime.cmis_object_parents("compat-tck", document["object_id"], context)
browser_document_parents = runtime.cmis_browser_parents("compat-tck", document["object_id"], context)
filtered_document = runtime.cmis_browser_object(
"compat-tck",
document["object_id"],
context,
property_filter="cmis:objectId,cmis:name",
include_allowable_actions=False,
include_acl=False,
)
filtered_children = runtime.cmis_browser_children(
"compat-tck",
context,
object_id=folder_object_id,
property_filter="cmis:objectId,cmis:name",
include_allowable_actions=False,
include_acl=False,
include_path_segment=False,
)
assert folder["path"] == "/TCK Workspace"
assert folder["properties"]["kontextual:workspaceFolder"] is True
@@ -222,11 +240,17 @@ def test_runtime_cmis_compat_profile_supports_workspace_folder_lifecycle(cmis_ru
assert fetched["properties"]["cmis:path"] == "/TCK Workspace"
assert parents["parents"][0]["object_id"] == "cmis-root"
assert document["path"] == "/TCK Workspace/Workspace Document"
assert document["properties"]["cmis:path"] == "/TCK Workspace/Workspace Document"
assert "cmis:path" not in document["properties"]
assert document_by_path["object_id"] == document["object_id"]
assert document_parents["count"] == 1
assert document_parents["parents"][0]["properties"]["cmis:path"] == "/TCK Workspace"
assert browser_document_parents[0]["relativePathSegment"] == "Workspace Document"
assert document["object_id"] in {item["object_id"] for item in folder_children["objects"]}
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"]
with pytest.raises(Exception) as exc_info:
runtime.cmis_delete_object("compat-tck", folder_object_id, {}, context)
@@ -242,6 +266,31 @@ def test_runtime_cmis_compat_profile_supports_workspace_folder_lifecycle(cmis_ru
assert "CMIS folder not found" in str(exc_info.value)
def test_runtime_cmis_workspace_folder_rename_keeps_object_id_stable(cmis_runtime) -> None:
runtime, context = cmis_runtime
folder = runtime.cmis_create_folder(
"compat-tck",
{"name": "Rename Source", "properties": {"cmis:objectTypeId": "cmis:folder"}},
context,
)
renamed = runtime.cmis_update_properties(
"compat-tck",
folder["object_id"],
{"properties": {"cmis:name": "Rename Target"}},
context,
)
fetched_by_old_id = runtime.cmis_object("compat-tck", folder["object_id"], context)
fetched_by_new_path = runtime.cmis_object_by_path("compat-tck", "/Rename Target", context)
deleted = runtime.cmis_delete_object("compat-tck", folder["object_id"], {}, context)
assert renamed["object_id"] == folder["object_id"]
assert renamed["path"] == "/Rename Target"
assert fetched_by_old_id["properties"]["cmis:name"] == "Rename Target"
assert fetched_by_new_path["object_id"] == folder["object_id"]
assert deleted["deleted"] is True
def test_runtime_cmis_rejects_unsupported_standard_property_updates(cmis_runtime) -> None:
runtime, context = cmis_runtime
@@ -249,7 +298,7 @@ def test_runtime_cmis_rejects_unsupported_standard_property_updates(cmis_runtime
runtime.cmis_update_properties(
"governed-authoring",
"cmis:asset:asset-runtime-source",
{"properties": {"cmis:name": "Renamed"}},
{"properties": {"cmis:objectTypeId": "cmis:folder"}},
context,
)