Setting up Open CMIS TCK

This commit is contained in:
2026-05-08 00:02:20 +02:00
parent 1ef1955fa0
commit 062af60af9
12 changed files with 433 additions and 38 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os
import subprocess import subprocess
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -45,6 +46,8 @@ def main() -> int:
parser.add_argument("--artifact-dir", type=Path, required=True) parser.add_argument("--artifact-dir", type=Path, required=True)
parser.add_argument("--run-dir", type=Path) parser.add_argument("--run-dir", type=Path)
parser.add_argument("--extension-path", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--extension-path", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--credentials-ref", default="")
parser.add_argument("--target-profile-dir", type=Path)
parser.add_argument("--user") parser.add_argument("--user")
parser.add_argument("--password") parser.add_argument("--password")
parser.add_argument("--maven-executable", default="mvn") parser.add_argument("--maven-executable", default="mvn")
@@ -60,7 +63,8 @@ def main() -> int:
def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]: def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
artifact_dir = args.artifact_dir.resolve() artifact_dir = args.artifact_dir.resolve()
artifact_dir.mkdir(parents=True, exist_ok=True) artifact_dir.mkdir(parents=True, exist_ok=True)
session_path = artifact_dir / "session.properties" private_session_path = artifact_dir / "session-private.properties"
redacted_session_path = artifact_dir / "session.properties.redacted"
groups_path = artifact_dir / "groups.txt" groups_path = artifact_dir / "groups.txt"
invocation_path = artifact_dir / "console-runner-invocation.json" invocation_path = artifact_dir / "console-runner-invocation.json"
console_stdout = artifact_dir / "console-runner-stdout.txt" console_stdout = artifact_dir / "console-runner-stdout.txt"
@@ -78,25 +82,41 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
[], [],
) )
_write_session_parameters(session_path, args) credentials = _load_credentials(args)
if credentials["status"] == "blocked":
return _result(
"blocked",
credentials["observations"],
args,
group_classes,
args.run_dir,
artifact_dir,
[],
extra_facts={"blocked_reason": "credentials_unavailable"},
)
_write_session_parameters(private_session_path, redacted_session_path, args, credentials)
_write_groups(groups_path, group_classes) _write_groups(groups_path, group_classes)
command = _maven_command(args, session_path, groups_path) command = _maven_command(args, private_session_path, groups_path)
invocation = { invocation = {
"created_at": _now(), "created_at": _now(),
"command": command, "command": command,
"check_group": args.check_group, "check_group": args.check_group,
"group_classes": group_classes, "group_classes": group_classes,
"session_parameters": str(session_path), "session_parameters": str(redacted_session_path),
"private_session_parameters": str(private_session_path),
"groups_file": str(groups_path), "groups_file": str(groups_path),
"auth_mode": credentials["auth_mode"],
} }
invocation_path.write_text(json.dumps(invocation, indent=2, sort_keys=True) + "\n", encoding="utf-8") invocation_path.write_text(json.dumps(invocation, indent=2, sort_keys=True) + "\n", encoding="utf-8")
artifact_refs = [ artifact_refs = [
str(session_path.relative_to(artifact_dir)), str(redacted_session_path.relative_to(artifact_dir)),
str(groups_path.relative_to(artifact_dir)), str(groups_path.relative_to(artifact_dir)),
str(invocation_path.relative_to(artifact_dir)), str(invocation_path.relative_to(artifact_dir)),
] ]
if not group_classes: if not group_classes:
private_session_path.unlink(missing_ok=True)
return _result( return _result(
"skipped", "skipped",
[f"No OpenCMIS TCK group classes are mapped for {args.check_group}; treated as a known-gap review group."], [f"No OpenCMIS TCK group classes are mapped for {args.check_group}; treated as a known-gap review group."],
@@ -107,6 +127,7 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
artifact_refs, artifact_refs,
) )
if args.dry_run: if args.dry_run:
private_session_path.unlink(missing_ok=True)
return _result( return _result(
"skipped", "skipped",
["Dry run completed; OpenCMIS TCK ConsoleRunner was not invoked."], ["Dry run completed; OpenCMIS TCK ConsoleRunner was not invoked."],
@@ -117,13 +138,16 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
artifact_refs, artifact_refs,
) )
completed = subprocess.run( try:
command, completed = subprocess.run(
capture_output=True, command,
text=True, capture_output=True,
timeout=args.timeout_seconds, text=True,
check=False, timeout=args.timeout_seconds,
) check=False,
)
finally:
private_session_path.unlink(missing_ok=True)
console_stdout.write_text(completed.stdout, encoding="utf-8") console_stdout.write_text(completed.stdout, encoding="utf-8")
console_stderr.write_text(completed.stderr, encoding="utf-8") console_stderr.write_text(completed.stderr, encoding="utf-8")
artifact_refs.extend( artifact_refs.extend(
@@ -151,7 +175,12 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
) )
def _write_session_parameters(path: Path, args: argparse.Namespace) -> None: def _write_session_parameters(
private_path: Path,
redacted_path: Path,
args: argparse.Namespace,
credentials: dict[str, Any],
) -> None:
values = { values = {
"org.apache.chemistry.opencmis.binding.spi.type": "browser", "org.apache.chemistry.opencmis.binding.spi.type": "browser",
"org.apache.chemistry.opencmis.binding.browser.url": args.browser_url, "org.apache.chemistry.opencmis.binding.browser.url": args.browser_url,
@@ -160,14 +189,99 @@ def _write_session_parameters(path: Path, args: argparse.Namespace) -> None:
"org.apache.chemistry.opencmis.binding.compression": "true", "org.apache.chemistry.opencmis.binding.compression": "true",
"org.apache.chemistry.opencmis.binding.cookies": "true", "org.apache.chemistry.opencmis.binding.cookies": "true",
} }
if args.user is not None: user = credentials.get("user")
values["org.apache.chemistry.opencmis.user"] = args.user password = credentials.get("password")
if args.password is not None: if isinstance(user, str) and user:
values["org.apache.chemistry.opencmis.password"] = args.password values["org.apache.chemistry.opencmis.user"] = user
path.write_text( if isinstance(password, str) and password:
values["org.apache.chemistry.opencmis.password"] = password
private_path.write_text(
"\n".join(f"{key}={value}" for key, value in values.items()) + "\n", "\n".join(f"{key}={value}" for key, value in values.items()) + "\n",
encoding="utf-8", encoding="utf-8",
) )
redacted_values = dict(values)
if "org.apache.chemistry.opencmis.password" in redacted_values:
redacted_values["org.apache.chemistry.opencmis.password"] = "<redacted>"
redacted_path.write_text(
"\n".join(f"{key}={value}" for key, value in redacted_values.items()) + "\n",
encoding="utf-8",
)
def _load_credentials(args: argparse.Namespace) -> dict[str, Any]:
if args.user is not None or args.password is not None:
return {
"status": "available",
"auth_mode": "argv",
"user": args.user,
"password": args.password,
}
credentials_ref = (args.credentials_ref or "").strip()
if not credentials_ref:
return {"status": "available", "auth_mode": "anonymous"}
if credentials_ref.startswith("env:"):
return _load_env_credentials(credentials_ref)
if credentials_ref.startswith("file:"):
return _load_file_credentials(credentials_ref, args.target_profile_dir)
return {
"status": "blocked",
"auth_mode": "unknown",
"observations": [
"Unsupported credentials_ref. Use env:USER_VAR,PASSWORD_VAR or file:/path/to/credentials.json."
],
}
def _load_env_credentials(credentials_ref: str) -> dict[str, Any]:
names = credentials_ref.removeprefix("env:").split(",", 1)
if len(names) != 2 or not names[0] or not names[1]:
return {
"status": "blocked",
"auth_mode": "env",
"observations": [
"Environment credentials_ref must be env:USER_VAR,PASSWORD_VAR."
],
}
user = os.environ.get(names[0])
password = os.environ.get(names[1])
missing = [name for name, value in [(names[0], user), (names[1], password)] if not value]
if missing:
return {
"status": "blocked",
"auth_mode": "env",
"observations": [
"Missing credential environment variable(s): " + ", ".join(missing) + "."
],
}
return {"status": "available", "auth_mode": "env", "user": user, "password": password}
def _load_file_credentials(
credentials_ref: str,
target_profile_dir: Path | None,
) -> dict[str, Any]:
raw_path = credentials_ref.removeprefix("file:")
path = Path(raw_path).expanduser()
if not path.is_absolute() and target_profile_dir is not None:
path = target_profile_dir / path
if not path.exists():
return {
"status": "blocked",
"auth_mode": "file",
"observations": [f"Credential file does not exist: {path}."],
}
payload = json.loads(path.read_text(encoding="utf-8"))
user = payload.get("user")
password = payload.get("password")
if not isinstance(user, str) or not isinstance(password, str):
return {
"status": "blocked",
"auth_mode": "file",
"observations": [
"Credential file must contain string fields 'user' and 'password'."
],
}
return {"status": "available", "auth_mode": "file", "user": user, "password": password}
def _write_groups(path: Path, group_classes: list[str]) -> None: def _write_groups(path: Path, group_classes: list[str]) -> None:
@@ -217,6 +331,7 @@ def _result(
artifact_refs: list[str], artifact_refs: list[str],
cases: list[dict[str, str]] | None = None, cases: list[dict[str, str]] | None = None,
returncode: int | None = None, returncode: int | None = None,
extra_facts: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
cases = cases or [] cases = cases or []
counts: dict[str, int] = {} counts: dict[str, int] = {}
@@ -224,18 +339,21 @@ def _result(
counts[case["status"]] = counts.get(case["status"], 0) + 1 counts[case["status"]] = counts.get(case["status"], 0) + 1
if not counts: if not counts:
counts[status] = 1 counts[status] = 1
facts = {
"adapter": "opencmis-console-runner",
"artifact_dir": str(artifact_dir),
"check_group": args.check_group,
"group_classes": group_classes,
"returncode": returncode,
"result_counts": counts,
}
if extra_facts:
facts.update(extra_facts)
return { return {
"result": status, "result": status,
"observations": observations, "observations": observations,
"tests": cases, "tests": cases,
"facts": { "facts": facts,
"adapter": "opencmis-console-runner",
"artifact_dir": str(artifact_dir),
"check_group": args.check_group,
"group_classes": group_classes,
"returncode": returncode,
"result_counts": counts,
},
"artifact_refs": [ "artifact_refs": [
_artifact_ref(artifact_dir / ref, run_dir, artifact_dir) _artifact_ref(artifact_dir / ref, run_dir, artifact_dir)
for ref in artifact_refs for ref in artifact_refs

View File

@@ -26,6 +26,53 @@ The CMIS target profile uses the guide-board `target-profile` schema:
- `known_gaps`: list unsupported optional requirements with a stable gap ID, - `known_gaps`: list unsupported optional requirements with a stable gap ID,
requirement refs, reason, and status such as `unsupported_by_design`. requirement refs, reason, and status such as `unsupported_by_design`.
Templates live under `profiles/targets/templates/`:
- `cmis-browser-anonymous.json`
- `cmis-browser-basic-auth-env.json`
- `cmis-browser-basic-auth-file.json`
## Credential References
Secrets should not be committed to the repository or preserved in guide-board
artifacts.
Supported credential reference forms:
```text
null
env:CMIS_TCK_USER,CMIS_TCK_PASSWORD
file:/absolute/path/to/cmis-tck-credentials.json
```
Environment variables are useful for local runs:
```sh
export CMIS_TCK_USER='alice'
export CMIS_TCK_PASSWORD='local-secret'
```
File credentials use JSON:
```json
{
"user": "alice",
"password": "local-secret"
}
```
The ConsoleRunner adapter writes a private session properties file only for the
duration of the run and retains `session.properties.redacted` as the artifact.
Validate target profiles through guide-board before running:
```sh
cd ../guide-board
PYTHONPATH=src python3 -m guide_board \
profile validate-target \
../open-cmis-tck/profiles/targets/kontextual-cmis-compat.json
```
## Assessment Runtime Fields ## Assessment Runtime Fields
Repository selection and harness execution settings live in the assessment Repository selection and harness execution settings live in the assessment
@@ -39,7 +86,28 @@ profile because they are run policy, not target identity:
"opencmis_tck": { "opencmis_tck": {
"repository_id": "compat-tck", "repository_id": "compat-tck",
"requires_java_maven": true, "requires_java_maven": true,
"command": ["java", "-jar", "/assets/opencmis-tck-runner.jar"] "command": [
"python3",
"{extension_path}/adapters/opencmis_console_adapter.py",
"--browser-url",
"{browser_url}",
"--repository-id",
"{repository_id}",
"--check-group",
"{check_group}",
"--artifact-dir",
"{artifact_dir}",
"--run-dir",
"{run_dir}",
"--extension-path",
"{extension_path}",
"--credentials-ref",
"{credentials_ref}",
"--target-profile-dir",
"{target_profile_dir}",
"--timeout-seconds",
"{timeout_seconds}"
]
} }
} }
} }

View File

@@ -84,7 +84,7 @@ adapters/opencmis_console_adapter.py
The adapter generates: The adapter generates:
- `session.properties` - `session.properties.redacted`
- `groups.txt` - `groups.txt`
- `console-runner-invocation.json` - `console-runner-invocation.json`
- `console-runner-stdout.txt` - `console-runner-stdout.txt`
@@ -114,7 +114,23 @@ org.apache.chemistry.opencmis.binding.cookies=true
``` ```
User and password fields are written only when explicitly supplied. Secret User and password fields are written only when explicitly supplied. Secret
storage is not implemented in this step. values are written to a private session file for the ConsoleRunner process and
removed after use; guide-board retains only `session.properties.redacted`.
Credential references come from the target profile:
```text
credentials_ref: null
credentials_ref: env:CMIS_TCK_USER,CMIS_TCK_PASSWORD
credentials_ref: file:/absolute/path/to/cmis-tck-credentials.json
```
For an authenticated local run:
```sh
export CMIS_TCK_USER='alice'
export CMIS_TCK_PASSWORD='local-secret'
```
## Check Group Mapping ## Check Group Mapping

View File

@@ -47,6 +47,10 @@
"{run_dir}", "{run_dir}",
"--extension-path", "--extension-path",
"{extension_path}", "{extension_path}",
"--credentials-ref",
"{credentials_ref}",
"--target-profile-dir",
"{target_profile_dir}",
"--timeout-seconds", "--timeout-seconds",
"{timeout_seconds}" "{timeout_seconds}"
] ]

View File

@@ -0,0 +1,23 @@
{
"id": "cmis-browser-anonymous-template",
"subject_type": "cmis-browser-binding-endpoint",
"subject_name": "Anonymous CMIS Browser Binding target",
"environment": "local",
"scope": [
"CMIS 1.1 Browser Binding compatibility preparation"
],
"endpoints": [
{
"id": "browser-binding",
"url": "http://127.0.0.1:8080/cmis/browser",
"binding": "cmis-browser"
}
],
"artifacts": [],
"credentials_ref": null,
"declared_capabilities": [
"cmis.repository-info",
"cmis.type-definitions"
],
"known_gaps": []
}

View File

@@ -0,0 +1,23 @@
{
"id": "cmis-browser-basic-auth-env-template",
"subject_type": "cmis-browser-binding-endpoint",
"subject_name": "Basic-auth CMIS Browser Binding target",
"environment": "local",
"scope": [
"CMIS 1.1 Browser Binding compatibility preparation"
],
"endpoints": [
{
"id": "browser-binding",
"url": "http://127.0.0.1:8080/cmis/browser",
"binding": "cmis-browser"
}
],
"artifacts": [],
"credentials_ref": "env:CMIS_TCK_USER,CMIS_TCK_PASSWORD",
"declared_capabilities": [
"cmis.repository-info",
"cmis.type-definitions"
],
"known_gaps": []
}

View File

@@ -0,0 +1,23 @@
{
"id": "cmis-browser-basic-auth-file-template",
"subject_type": "cmis-browser-binding-endpoint",
"subject_name": "File-credential CMIS Browser Binding target",
"environment": "local",
"scope": [
"CMIS 1.1 Browser Binding compatibility preparation"
],
"endpoints": [
{
"id": "browser-binding",
"url": "http://127.0.0.1:8080/cmis/browser",
"binding": "cmis-browser"
}
],
"artifacts": [],
"credentials_ref": "file:/absolute/path/to/cmis-tck-credentials.json",
"declared_capabilities": [
"cmis.repository-info",
"cmis.type-definitions"
],
"known_gaps": []
}

View File

@@ -0,0 +1,4 @@
{
"user": "cmis-user",
"password": "replace-with-local-secret"
}

View File

@@ -138,6 +138,7 @@ def _run_configured_tck(
"target_profile_id": context["target_profile"]["id"], "target_profile_id": context["target_profile"]["id"],
"repository_id": _repository_id(context), "repository_id": _repository_id(context),
"browser_binding_url": _browser_url(context), "browser_binding_url": _browser_url(context),
"credentials_ref": _credentials_ref(context),
}, },
) )
@@ -424,6 +425,8 @@ def _expand_arg(
.replace("{extension_path}", context["extension_path"]) .replace("{extension_path}", context["extension_path"])
.replace("{browser_url}", _browser_url(context) or "") .replace("{browser_url}", _browser_url(context) or "")
.replace("{repository_id}", _repository_id(context) or "") .replace("{repository_id}", _repository_id(context) or "")
.replace("{credentials_ref}", _credentials_ref(context) or "")
.replace("{target_profile_dir}", _target_profile_dir(context) or "")
.replace("{check_group}", selected_group or "") .replace("{check_group}", selected_group or "")
.replace("{target_id}", context["target_profile"]["id"]) .replace("{target_id}", context["target_profile"]["id"])
.replace("{timeout_seconds}", str(int(_timeout_seconds(context)))) .replace("{timeout_seconds}", str(int(_timeout_seconds(context))))
@@ -442,6 +445,18 @@ def _repository_id(context: dict[str, Any]) -> str | None:
return value if isinstance(value, str) else None return value if isinstance(value, str) else None
def _credentials_ref(context: dict[str, Any]) -> str | None:
value = context["target_profile"].get("credentials_ref")
return value if isinstance(value, str) else None
def _target_profile_dir(context: dict[str, Any]) -> str | None:
path = context["plan"].get("profile_paths", {}).get("target_profile_path")
if not isinstance(path, str) or not path:
return None
return str(Path(path).resolve().parent)
def _timeout_seconds(context: dict[str, Any]) -> float: def _timeout_seconds(context: dict[str, Any]) -> float:
runtime_policy = context["assessment_profile"].get("runtime_policy", {}) runtime_policy = context["assessment_profile"].get("runtime_policy", {})
opencmis_policy = _opencmis_policy(context) opencmis_policy = _opencmis_policy(context)

View File

@@ -118,12 +118,25 @@ def validate_cmis_profile_config(
repository_id = None repository_id = None
if isinstance(opencmis_policy, dict): if isinstance(opencmis_policy, dict):
repository_id = opencmis_policy.get("repository_id") repository_id = opencmis_policy.get("repository_id")
if repository_id is not None and not isinstance(repository_id, str): if repository_id is not None and not isinstance(repository_id, str):
diagnostics.append(
_diagnostic(
"error",
"runtime_policy.opencmis_tck.repository_id",
"repository_id must be a string when configured.",
)
)
credentials_ref = target_profile.get("credentials_ref")
auth_mode = "anonymous"
if isinstance(credentials_ref, str) and credentials_ref:
auth_mode = _auth_mode(credentials_ref)
if auth_mode == "unknown":
diagnostics.append( diagnostics.append(
_diagnostic( _diagnostic(
"error", "warning",
"runtime_policy.opencmis_tck.repository_id", "credentials_ref",
"repository_id must be a string when configured.", "Use null, env:USER_VAR,PASSWORD_VAR, or file:/path/to/credentials.json.",
) )
) )
@@ -134,7 +147,8 @@ def validate_cmis_profile_config(
"cmis_config": { "cmis_config": {
"browser_binding_url": browser_endpoints[0]["url"] if browser_endpoints else None, "browser_binding_url": browser_endpoints[0]["url"] if browser_endpoints else None,
"repository_id": repository_id, "repository_id": repository_id,
"auth_mode": "anonymous" if target_profile.get("credentials_ref") is None else "credentials_ref", "auth_mode": auth_mode,
"credentials_ref": credentials_ref,
"declared_capabilities": sorted(declared), "declared_capabilities": sorted(declared),
"known_gap_refs": sorted(known_gap_refs), "known_gap_refs": sorted(known_gap_refs),
"timeout_seconds": timeout, "timeout_seconds": timeout,
@@ -144,3 +158,11 @@ def validate_cmis_profile_config(
def _diagnostic(severity: str, field: str, message: str) -> dict[str, str]: def _diagnostic(severity: str, field: str, message: str) -> dict[str, str]:
return {"severity": severity, "field": field, "message": message} return {"severity": severity, "field": field, "message": message}
def _auth_mode(credentials_ref: str) -> str:
if credentials_ref.startswith("env:"):
return "env"
if credentials_ref.startswith("file:"):
return "file"
return "unknown"

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import http.client import http.client
import json import json
import os
import subprocess import subprocess
import sys import sys
import threading import threading
@@ -13,7 +14,11 @@ from tempfile import TemporaryDirectory
from guide_board.discovery import discover_extensions from guide_board.discovery import discover_extensions
from guide_board.execution import run_assessment from guide_board.execution import run_assessment
from guide_board.planning import build_run_plan, validate_assessment_profile from guide_board.planning import (
build_run_plan,
validate_assessment_profile,
validate_target_profile,
)
from guide_board.retention import build_trend_summary from guide_board.retention import build_trend_summary
from guide_board.service import ServiceHandle, start_service from guide_board.service import ServiceHandle, start_service
from open_cmis_tck.bootstrap import TCK_COORDINATE, check_runtime from open_cmis_tck.bootstrap import TCK_COORDINATE, check_runtime
@@ -74,6 +79,11 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
self.assertEqual(diagnostics["cmis_config"]["repository_id"], "compat-tck") self.assertEqual(diagnostics["cmis_config"]["repository_id"], "compat-tck")
self.assertEqual(diagnostics["cmis_config"]["auth_mode"], "anonymous") self.assertEqual(diagnostics["cmis_config"]["auth_mode"], "anonymous")
authenticated = dict(target)
authenticated["credentials_ref"] = "env:CMIS_TCK_USER,CMIS_TCK_PASSWORD"
authenticated_diagnostics = validate_cmis_profile_config(authenticated, assessment)
self.assertEqual(authenticated_diagnostics["cmis_config"]["auth_mode"], "env")
broken = dict(target) broken = dict(target)
broken["endpoints"] = [] broken["endpoints"] = []
broken_diagnostics = validate_cmis_profile_config(broken, assessment) broken_diagnostics = validate_cmis_profile_config(broken, assessment)
@@ -83,6 +93,17 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
broken_diagnostics["diagnostics"][0]["message"], broken_diagnostics["diagnostics"][0]["message"],
) )
def test_target_profile_templates_validate(self) -> None:
template_dir = ROOT / "profiles" / "targets" / "templates"
for name in [
"cmis-browser-anonymous.json",
"cmis-browser-basic-auth-env.json",
"cmis-browser-basic-auth-file.json",
]:
with self.subTest(name=name):
profile = validate_target_profile(template_dir / name)
self.assertEqual(profile["subject_type"], "cmis-browser-binding-endpoint")
def test_bootstrap_reports_local_tck_runtime_posture(self) -> None: def test_bootstrap_reports_local_tck_runtime_posture(self) -> None:
with TemporaryDirectory() as temporary_directory: with TemporaryDirectory() as temporary_directory:
output = Path(temporary_directory) / "runtime-summary.json" output = Path(temporary_directory) / "runtime-summary.json"
@@ -129,17 +150,65 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
self.assertEqual(completed.returncode, 0) self.assertEqual(completed.returncode, 0)
self.assertEqual(result["result"], "skipped") self.assertEqual(result["result"], "skipped")
self.assertIn( self.assertIn(
"artifacts/open-cmis-tck/tck/repository-type/session.properties", "artifacts/open-cmis-tck/tck/repository-type/session.properties.redacted",
result["artifact_refs"], result["artifact_refs"],
) )
self.assertIn( self.assertIn(
"org.apache.chemistry.opencmis.binding.spi.type=browser", "org.apache.chemistry.opencmis.binding.spi.type=browser",
(artifact_dir / "session.properties").read_text(encoding="utf-8"), (artifact_dir / "session.properties.redacted").read_text(encoding="utf-8"),
) )
self.assertIn( self.assertIn(
"org.apache.chemistry.opencmis.tck.tests.basics.BasicsTestGroup", "org.apache.chemistry.opencmis.tck.tests.basics.BasicsTestGroup",
(artifact_dir / "groups.txt").read_text(encoding="utf-8"), (artifact_dir / "groups.txt").read_text(encoding="utf-8"),
) )
self.assertFalse((artifact_dir / "session-private.properties").exists())
def test_console_adapter_uses_env_credentials_without_persisting_secret(self) -> None:
with TemporaryDirectory() as temporary_directory:
temp_root = Path(temporary_directory)
run_dir = temp_root / "run"
artifact_dir = run_dir / "artifacts" / "open-cmis-tck" / "tck" / "repository-type"
env = {
**os.environ,
"CMIS_TEST_USER": "alice",
"CMIS_TEST_PASSWORD": "secret-password",
}
completed = subprocess.run(
[
sys.executable,
str(ROOT / "adapters" / "opencmis_console_adapter.py"),
"--browser-url",
"http://127.0.0.1:8000/cmis/browser",
"--repository-id",
"local-test-repository",
"--check-group",
"repository-type",
"--artifact-dir",
str(artifact_dir),
"--run-dir",
str(run_dir),
"--extension-path",
str(ROOT),
"--credentials-ref",
"env:CMIS_TEST_USER,CMIS_TEST_PASSWORD",
"--dry-run",
],
capture_output=True,
text=True,
check=False,
env=env,
)
result = json.loads(completed.stdout)
redacted = (artifact_dir / "session.properties.redacted").read_text(
encoding="utf-8"
)
self.assertEqual(completed.returncode, 0)
self.assertEqual(result["result"], "skipped")
self.assertIn("org.apache.chemistry.opencmis.user=alice", redacted)
self.assertIn("org.apache.chemistry.opencmis.password=<redacted>", redacted)
self.assertNotIn("secret-password", redacted)
self.assertFalse((artifact_dir / "session-private.properties").exists())
def test_runs_cmis_preflight_against_local_endpoint(self) -> None: def test_runs_cmis_preflight_against_local_endpoint(self) -> None:
server = HTTPServer(("127.0.0.1", 0), _CmisHandler) server = HTTPServer(("127.0.0.1", 0), _CmisHandler)

View File

@@ -160,7 +160,7 @@ Progress:
```task ```task
id: OPEN-CMIS-TCK-WP-0002-T004 id: OPEN-CMIS-TCK-WP-0002-T004
status: todo status: done
priority: high priority: high
state_hub_task_id: "c33a4d9a-a398-4a02-93c2-1d38e62a578f" state_hub_task_id: "c33a4d9a-a398-4a02-93c2-1d38e62a578f"
``` ```
@@ -176,6 +176,16 @@ Acceptance:
- Documentation explains how to run a profile validation before starting a TCK - Documentation explains how to run a profile validation before starting a TCK
run. run.
Progress:
- Added anonymous, environment-credential, and file-credential target profile
templates under `profiles/targets/templates/`.
- Added credential reference diagnostics to `open_cmis_tck.profile`.
- Wired `credentials_ref` and target profile directory expansion into the
OpenCMIS wrapper command placeholders.
- Updated the ConsoleRunner adapter to support `env:` and `file:` credential
refs without retaining raw passwords in evidence artifacts.
## D2.5 - Real Result Normalization ## D2.5 - Real Result Normalization
```task ```task