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:", ]