diff --git a/docs/cmis-profiled-access-points-implementation.md b/docs/cmis-profiled-access-points-implementation.md index 600dcea..4a21fd9 100644 --- a/docs/cmis-profiled-access-points-implementation.md +++ b/docs/cmis-profiled-access-points-implementation.md @@ -120,6 +120,22 @@ Relationship listings and change logs now apply the same asset visibility gates as object reads. This prevents indirect leakage of confidential or restricted asset IDs through relationship targets or audit-backed change entries. +## Fixture And Optional TCK Integration + +CMIS fixtures now act as active compatibility contracts: + +- `examples/cmis/capability-fixtures.json` defines profile expectations and + capability groups, +- `tests/cmis/test_cmis_fixture_integration.py` compares those expectations to + implemented profiles and access-point shapes, +- `tests/cmis/opencmis-tck/tck-subset-map.json` maps fixture capability groups + to selected OpenCMIS TCK groups, +- `tests/cmis/opencmis-tck/tck-result-template.json` captures optional TCK + result summaries and known capability gaps. + +The default Python suite validates the fixture/TCK mapping without requiring +Java or Maven. Actual OpenCMIS TCK execution remains opt-in. + Route-level tests are present but skip when the optional FastAPI/httpx service dependencies are not installed. Runtime-level Browser Binding tests cover the same behavior in the default Python test suite. diff --git a/examples/cmis/capability-fixtures.json b/examples/cmis/capability-fixtures.json index 240a8f6..29a4237 100644 --- a/examples/cmis/capability-fixtures.json +++ b/examples/cmis/capability-fixtures.json @@ -12,7 +12,7 @@ "binding": "browser", "mutations": false, "visibility": ["public", "internal"], - "deny": ["confidential"], + "deny": ["confidential", "restricted"], "expected_tck_posture": "repository/type/read/navigation/query/content-read subset" }, "governed-authoring": { @@ -20,14 +20,14 @@ "binding": "browser", "mutations": true, "visibility": ["public", "internal"], - "deny": ["confidential"], + "deny": ["confidential", "restricted"], "expected_tck_posture": "readonly subset plus selected create/update/delete checks" }, "admin-export": { "description": "Service-account export profile for broad governance inspection.", "binding": "browser", "mutations": false, - "visibility": ["public", "internal", "confidential"], + "visibility": ["public", "internal", "confidential", "restricted"], "deny": [], "expected_tck_posture": "internal contract tests, not general client compatibility" }, @@ -36,7 +36,7 @@ "binding": "browser", "mutations": true, "visibility": ["public", "internal"], - "deny": ["confidential"], + "deny": ["confidential", "restricted"], "expected_tck_posture": "selected OpenCMIS TCK Browser Binding subset" } }, @@ -200,4 +200,3 @@ } } } - diff --git a/tests/cmis/opencmis-tck/README.md b/tests/cmis/opencmis-tck/README.md index 4321cc4..89521ec 100644 --- a/tests/cmis/opencmis-tck/README.md +++ b/tests/cmis/opencmis-tck/README.md @@ -26,3 +26,15 @@ The first target is a selected Browser Binding subset: Mutation groups should only be enabled for `governed-authoring` or `compat-tck` profiles after the policy and audit gates are implemented. +Harness files: + +- `tck-subset-map.json` maps engine capability groups to selected OpenCMIS TCK + groups and known gaps. +- `tck-result-template.json` is the compact result capture format for optional + TCK runs. Results should be stored outside the default repo history unless + they document an intentional compatibility baseline. + +Default `pytest` does not execute Java/Maven. It validates the map and result +template so the optional harness does not drift away from the implemented +profiles and fixtures. + diff --git a/tests/cmis/opencmis-tck/tck-result-template.json b/tests/cmis/opencmis-tck/tck-result-template.json new file mode 100644 index 0000000..625c9f1 --- /dev/null +++ b/tests/cmis/opencmis-tck/tck-result-template.json @@ -0,0 +1,28 @@ +{ + "schema_version": 1, + "run": { + "tool": "Apache Chemistry OpenCMIS TCK", + "tool_version": "1.1.0", + "profile": "compat-tck", + "repository_id": "kontextual-compat-tck", + "browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser", + "started_at": null, + "finished_at": null + }, + "summary": { + "passed": 0, + "failed": 0, + "skipped": 0, + "known_gap": 0 + }, + "groups": [ + { + "capability_group": "repository-type", + "tck_group": "RepositoryInfoTestGroup", + "status": "not_run", + "notes": "" + } + ], + "capability_gaps": [] +} + diff --git a/tests/cmis/opencmis-tck/tck-subset-map.json b/tests/cmis/opencmis-tck/tck-subset-map.json new file mode 100644 index 0000000..6a6f7e6 --- /dev/null +++ b/tests/cmis/opencmis-tck/tck-subset-map.json @@ -0,0 +1,66 @@ +{ + "schema_version": 1, + "tck": { + "name": "Apache Chemistry OpenCMIS TCK", + "version": "1.1.0", + "artifact": "org.apache.chemistry.opencmis:chemistry-opencmis-test-tck:1.1.0", + "execution": "optional" + }, + "target": { + "profile": "compat-tck", + "repository_id": "kontextual-compat-tck", + "browser_url": "http://127.0.0.1:8000/cmis/compat-tck/browser" + }, + "groups": [ + { + "capability_group": "repository-type", + "tck_groups": ["RepositoryInfoTestGroup", "TypesTestGroup"], + "expected": "selected-pass" + }, + { + "capability_group": "navigation", + "tck_groups": ["NavigationTestGroup"], + "expected": "selected-pass" + }, + { + "capability_group": "object-content", + "tck_groups": ["ObjectServiceTestGroup", "ContentStreamTestGroup"], + "expected": "selected-pass" + }, + { + "capability_group": "versioning", + "tck_groups": ["VersioningTestGroup"], + "expected": "partial-pass", + "known_gaps": ["private_working_copy"] + }, + { + "capability_group": "discovery-query", + "tck_groups": ["QueryTestGroup"], + "expected": "partial-pass", + "known_gaps": ["full_cmis_sql_joins"] + }, + { + "capability_group": "relationships", + "tck_groups": ["RelationshipTestGroup"], + "expected": "selected-pass" + }, + { + "capability_group": "acl-policy", + "tck_groups": ["AclTestGroup", "PolicyTestGroup"], + "expected": "partial-pass", + "known_gaps": ["apply_policy", "remove_policy"] + }, + { + "capability_group": "change-log", + "tck_groups": ["ChangeLogTestGroup"], + "expected": "selected-pass" + }, + { + "capability_group": "retention-renditions-bulk", + "tck_groups": ["RenditionsTestGroup", "BulkUpdateTestGroup"], + "expected": "skip", + "known_gaps": ["retention_hold_mutation", "bulk_update_properties", "rendition_streams"] + } + ] +} + diff --git a/tests/cmis/test_cmis_fixture_integration.py b/tests/cmis/test_cmis_fixture_integration.py new file mode 100644 index 0000000..27704d8 --- /dev/null +++ b/tests/cmis/test_cmis_fixture_integration.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from kontextual_engine import ( + Actor, + ActorType, + CMISAccessPoint, + CMISAccessProfile, + CMISAction, + OperationContext, +) + + +pytestmark = pytest.mark.cmis + +ROOT = Path(__file__).resolve().parents[2] +FIXTURE_PATH = ROOT / "examples" / "cmis" / "capability-fixtures.json" +TCK_MAP_PATH = ROOT / "tests" / "cmis" / "opencmis-tck" / "tck-subset-map.json" +TCK_RESULT_TEMPLATE = ROOT / "tests" / "cmis" / "opencmis-tck" / "tck-result-template.json" + +ACTION_MAP = { + "create_document": CMISAction.CREATE_DOCUMENT, + "update_properties": CMISAction.UPDATE_PROPERTIES, + "delete_object": CMISAction.DELETE_OBJECT, + "set_content_stream": CMISAction.SET_CONTENT_STREAM, +} + + +def _catalog() -> dict: + return json.loads(FIXTURE_PATH.read_text(encoding="utf-8")) + + +def _tck_map() -> dict: + return json.loads(TCK_MAP_PATH.read_text(encoding="utf-8")) + + +def _profiles() -> dict[str, CMISAccessProfile]: + return { + profile.name: profile + for profile in ( + CMISAccessProfile.readonly_browser(), + CMISAccessProfile.governed_authoring(), + CMISAccessProfile.admin_export(), + CMISAccessProfile.compat_tck(), + ) + } + + +def _context(actor_type: ActorType = ActorType.HUMAN) -> OperationContext: + return OperationContext.create( + Actor.create(actor_type, actor_id=f"fixture-{actor_type.value}"), + correlation_id="corr-cmis-fixture-integration", + ) + + +def test_fixture_profiles_match_implemented_access_profiles() -> None: + catalog = _catalog() + implemented = _profiles() + + assert set(catalog["profiles"]) == set(implemented) + for profile_name, fixture in catalog["profiles"].items(): + profile = implemented[profile_name] + assert profile.binding.value == fixture["binding"] + assert profile.allow_mutations is fixture["mutations"] + assert [item.value for item in profile.visible_sensitivities] == fixture["visibility"] + assert [item.value for item in profile.denied_sensitivities] == fixture["deny"] + + +def test_fixture_mutation_expectations_match_profile_decisions() -> None: + catalog = _catalog() + implemented = _profiles() + + for profile_name, expectations in catalog["profile_expectations"].items(): + profile = implemented[profile_name] + context = _context( + ActorType.SERVICE_ACCOUNT if profile.required_actor_types else ActorType.HUMAN + ) + for action_name in expectations.get("must_authorize_actions", []): + assert profile.decide_action(ACTION_MAP[action_name], context).allowed is True + for action_name in expectations.get("must_reject_actions", []): + assert profile.decide_action(ACTION_MAP[action_name], context).allowed is False + + +def test_every_fixture_profile_has_a_browser_access_point_shape() -> None: + for profile in _profiles().values(): + access_point = CMISAccessPoint( + access_point_id=profile.name, + repository_id=f"kontextual-{profile.name}", + profile=profile, + base_path=f"/cmis/{profile.name}/browser", + ) + serialized = access_point.to_dict() + + assert serialized["base_path"] == f"/cmis/{profile.name}/browser" + assert serialized["repository_id"] == f"kontextual-{profile.name}" + assert serialized["profile"]["binding"] == "browser" + + +def test_optional_opencmis_tck_map_covers_fixture_capability_groups() -> None: + catalog = _catalog() + tck_map = _tck_map() + fixture_groups = {group["id"] for group in catalog["capability_groups"]} + mapped_groups = {group["capability_group"] for group in tck_map["groups"]} + + assert tck_map["tck"]["execution"] == "optional" + assert tck_map["target"]["profile"] == "compat-tck" + assert mapped_groups == fixture_groups + assert all(group["tck_groups"] for group in tck_map["groups"]) + + +def test_optional_tck_result_template_can_capture_gap_mapping() -> None: + template = json.loads(TCK_RESULT_TEMPLATE.read_text(encoding="utf-8")) + + assert template["run"]["profile"] == "compat-tck" + assert template["summary"] == {"passed": 0, "failed": 0, "skipped": 0, "known_gap": 0} + assert "capability_gaps" in template + assert template["groups"][0]["status"] == "not_run" + diff --git a/workplans/KONT-WP-0012-cmis-profiled-access-points.md b/workplans/KONT-WP-0012-cmis-profiled-access-points.md index 29b1e3b..0f65189 100644 --- a/workplans/KONT-WP-0012-cmis-profiled-access-points.md +++ b/workplans/KONT-WP-0012-cmis-profiled-access-points.md @@ -46,6 +46,8 @@ suite. - `tests/cmis/test_cmis_domain_mapper.py` - `tests/cmis/test_cmis_runtime_browser_binding.py` - `tests/cmis/test_cmis_browser_binding_api.py` +- `tests/cmis/test_cmis_fixture_integration.py` +- `tests/cmis/opencmis-tck/tck-subset-map.json` ## Architecture Constraint @@ -141,7 +143,7 @@ Acceptance: ```task id: KONT-WP-0012-T006 -status: todo +status: done priority: medium state_hub_task_id: "2f1e9075-395e-4ed0-9abd-ed7c4ecd774d" ```