Publish curated ITWorx_MCP_Hub source from 6c7434368ce2a594005983c5a6601a31a639e7f1
This commit is contained in:
1 parent
77946d62c4
commit
6d40ab975c
86 files changed
+6784
-1478
No files matched your search
+29
-4
@@ -11,8 +11,10 @@ authorization, and the real policy engine -- not a mocked frontend:
|
||||
-> fake ContextForge (in-process MockTransport; nothing the
|
||||
browser talks to directly, so no real HTTP needed here)
|
||||
|
||||
Each subcommand below blocks running one of these processes; Playwright's
|
||||
`webServer` config (apps/web/playwright.config.ts) starts all three and
|
||||
The worker readiness dependency is an explicit offline fixture; worker reconciliation
|
||||
is validated by separate integration tests. Each subcommand below blocks running
|
||||
one of these processes; Playwright's
|
||||
`webServer` config (apps/web/playwright.config.ts) starts all four and
|
||||
waits for each to answer its readiness probe before running any test.
|
||||
|
||||
Persona switching: `OIDCSettings`/the real `/auth/login` route were left
|
||||
@@ -37,6 +39,7 @@ if str(ROOT) not in sys.path:
|
||||
|
||||
import httpx # noqa: E402
|
||||
import uvicorn # noqa: E402
|
||||
from fastapi import FastAPI # noqa: E402
|
||||
|
||||
from services.control_api.auth.config import OIDCSettings # noqa: E402
|
||||
from services.control_api.auth.fake_provider import FakeOIDCProvider # noqa: E402
|
||||
@@ -124,13 +127,29 @@ def run_control_api(port: int, oidc_port: int, web_port: int) -> None:
|
||||
uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")
|
||||
|
||||
|
||||
def run_web(port: int, control_port: int, dist_root: Path) -> None:
|
||||
def worker_readiness_fixture() -> FastAPI:
|
||||
"""Offline readiness dependency; worker reconciliation has separate integration tests."""
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/health/ready")
|
||||
def ready() -> dict[str, str]:
|
||||
return {"status": "ready", "component": "e2e-worker-fixture"}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run_worker(port: int) -> None:
|
||||
uvicorn.run(worker_readiness_fixture(), host="127.0.0.1", port=port, log_level="warning")
|
||||
|
||||
|
||||
def run_web(port: int, control_port: int, worker_port: int, dist_root: Path) -> None:
|
||||
if not (dist_root / "index.html").is_file():
|
||||
raise SystemExit(
|
||||
f"{dist_root} has no built frontend; "
|
||||
"run `pnpm --filter @itworx-mcp-hub/web build` first"
|
||||
)
|
||||
os.environ["HUB_CONTROL_URL"] = f"http://127.0.0.1:{control_port}"
|
||||
os.environ["HUB_WORKER_URL"] = f"http://127.0.0.1:{worker_port}"
|
||||
os.environ["HUB_WEB_ROOT"] = str(dist_root)
|
||||
tls = generate_local_tls_material(STATE_DIR / "tls-web")
|
||||
uvicorn.run(
|
||||
@@ -158,15 +177,21 @@ def main() -> None:
|
||||
web_parser = sub.add_parser("web")
|
||||
web_parser.add_argument("--port", type=int, required=True)
|
||||
web_parser.add_argument("--control-port", type=int, required=True)
|
||||
web_parser.add_argument("--worker-port", type=int, required=True)
|
||||
web_parser.add_argument("--dist", type=Path, required=True)
|
||||
|
||||
worker_parser = sub.add_parser("worker")
|
||||
worker_parser.add_argument("--port", type=int, required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "oidc":
|
||||
run_oidc(args.port)
|
||||
elif args.command == "control-api":
|
||||
run_control_api(args.port, args.oidc_port, args.web_port)
|
||||
elif args.command == "web":
|
||||
run_web(args.port, args.control_port, args.dist)
|
||||
run_web(args.port, args.control_port, args.worker_port, args.dist)
|
||||
elif args.command == "worker":
|
||||
run_worker(args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -33,14 +33,8 @@ def test_appops_tool_catalog_is_bijective_and_server_namespaced() -> None:
|
||||
PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.preflight"]
|
||||
== "appops.self_upgrade.preflight"
|
||||
)
|
||||
assert (
|
||||
PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.apply"]
|
||||
== "appops.self_upgrade.apply"
|
||||
)
|
||||
assert (
|
||||
PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.status"]
|
||||
== "appops.self_upgrade.status"
|
||||
)
|
||||
assert PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.apply"] == "appops.self_upgrade.apply"
|
||||
assert PUBLIC_TO_UPSTREAM["server.appops.self_upgrade.status"] == "appops.self_upgrade.status"
|
||||
|
||||
|
||||
def test_every_public_appops_tool_has_governance_metadata() -> None:
|
||||
@@ -60,9 +54,7 @@ def test_appops_self_upgrade_manifests_are_fixed_target_and_fingerprint_bound()
|
||||
for path in (ROOT / "catalog" / "tools").glob("*.yaml"):
|
||||
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
spec = document.get("spec") if isinstance(document, dict) else None
|
||||
if isinstance(spec, dict) and spec.get("canonicalName", "").startswith(
|
||||
"server.appops."
|
||||
):
|
||||
if isinstance(spec, dict) and spec.get("canonicalName", "").startswith("server.appops."):
|
||||
specs[spec["canonicalName"]] = spec
|
||||
|
||||
assert set(specs) == {
|
||||
@@ -84,9 +76,10 @@ def test_appops_self_upgrade_manifests_are_fixed_target_and_fingerprint_bound()
|
||||
apply = specs["server.appops.self_upgrade.apply"]
|
||||
assert apply["riskClass"] == "R3"
|
||||
assert apply["readOnly"] is False
|
||||
assert apply["inputSchema"]["properties"]["expected_preflight_fingerprint"][
|
||||
"pattern"
|
||||
] == "^[0-9a-f]{64}$"
|
||||
assert (
|
||||
apply["inputSchema"]["properties"]["expected_preflight_fingerprint"]["pattern"]
|
||||
== "^[0-9a-f]{64}$"
|
||||
)
|
||||
assert {
|
||||
"expected_preflight_fingerprint",
|
||||
"idempotency_key",
|
||||
|
||||
@@ -76,6 +76,11 @@ class BootstrapTests(unittest.TestCase):
|
||||
"apps/web/src/rbac-context.tsx",
|
||||
"apps/web/src/navigation.ts",
|
||||
"apps/web/src/routing.ts",
|
||||
"apps/web/src/operator-data.ts",
|
||||
"apps/web/src/preferences.ts",
|
||||
"apps/web/src/schema-input.ts",
|
||||
"apps/web/src/status-refresh.ts",
|
||||
"apps/web/src/tool-history.ts",
|
||||
"apps/web/src/components/Brand.tsx",
|
||||
"apps/web/src/components/Breadcrumbs.tsx",
|
||||
"apps/web/src/components/CommandMenu.tsx",
|
||||
@@ -88,9 +93,13 @@ class BootstrapTests(unittest.TestCase):
|
||||
"apps/web/src/components/Modal.tsx",
|
||||
"apps/web/src/components/SessionMenu.tsx",
|
||||
"apps/web/src/components/StatusPill.tsx",
|
||||
"apps/web/src/components/RecentToolRuns.tsx",
|
||||
"apps/web/src/components/SchemaInputFields.tsx",
|
||||
"apps/web/src/components/modal-focus.ts",
|
||||
"apps/web/src/pages/AuthGateway.tsx",
|
||||
"apps/web/src/pages/CatalogWorkflow.tsx",
|
||||
"apps/web/src/pages/ClientOnboarding.tsx",
|
||||
"apps/web/src/pages/ConnectorDetail.tsx",
|
||||
"apps/web/src/pages/Dashboard.tsx",
|
||||
"apps/web/src/pages/ForbiddenPanel.tsx",
|
||||
"apps/web/src/pages/Operations.tsx",
|
||||
@@ -102,6 +111,10 @@ class BootstrapTests(unittest.TestCase):
|
||||
"apps/web/tests/contracts.test.ts",
|
||||
"apps/web/tests/api.test.ts",
|
||||
"apps/web/tests/rbac.test.ts",
|
||||
"apps/web/tests/operator-data.test.ts",
|
||||
"apps/web/tests/preferences.test.ts",
|
||||
"apps/web/tests/schema-input.test.ts",
|
||||
"apps/web/tests/tool-history.test.ts",
|
||||
"apps/web/tests/e2e/fixtures.ts",
|
||||
"apps/web/tests/e2e/auth.spec.ts",
|
||||
"apps/web/tests/e2e/rbac.spec.ts",
|
||||
@@ -110,6 +123,10 @@ class BootstrapTests(unittest.TestCase):
|
||||
"apps/web/tests/e2e/navigation.spec.ts",
|
||||
"apps/web/tests/e2e/accessibility.spec.ts",
|
||||
"apps/web/tests/e2e/visual-quality.spec.ts",
|
||||
"apps/web/tests/e2e/operator-matrix.spec.ts",
|
||||
"apps/web/tests/e2e/operator-shell.spec.ts",
|
||||
"apps/web/tests/e2e/operator-review.spec.ts",
|
||||
"apps/web/tests/e2e/operator-workflows.spec.ts",
|
||||
"apps/web/src/main.ts",
|
||||
"apps/web/tests/bootstrap.test.ts",
|
||||
"apps/web/vite.config.ts",
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -30,6 +31,59 @@ def adapter_for(fake: FakeContextForge) -> ContextForgeClient:
|
||||
|
||||
|
||||
class CatalogLoaderTests(unittest.TestCase):
|
||||
def test_schema_store_loads_once_per_catalog_load_without_changing_validation(self) -> None:
|
||||
class CountingCatalogLoader(CatalogLoader):
|
||||
store_loads = 0
|
||||
|
||||
def _schema_store(self) -> dict[str, Any]:
|
||||
self.store_loads += 1
|
||||
return super()._schema_store()
|
||||
|
||||
loader = CountingCatalogLoader(ROOT / "catalog")
|
||||
report = loader.load()
|
||||
self.assertTrue(report.valid, report.issues)
|
||||
self.assertEqual(len(report.documents), 281)
|
||||
self.assertEqual(report.issues, ())
|
||||
self.assertEqual(loader.store_loads, 1)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory) / "catalog"
|
||||
project_dir = root / "projects"
|
||||
project_dir.mkdir(parents=True)
|
||||
payload = json.loads(
|
||||
(ROOT / "catalog/projects/general-infrastructure.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
payload["spec"]["virtualServers"][0]["endpointAlias"] = "not-an-alias"
|
||||
payload["spec"]["capabilityPacks"] = []
|
||||
payload["spec"]["connectorBindings"] = []
|
||||
payload["spec"]["credentialToken"] = "plain-secret-value"
|
||||
(project_dir / "bad.yaml").write_text(json.dumps(payload), encoding="utf-8")
|
||||
invalid_loader = CountingCatalogLoader(root, schema_root=ROOT / "contracts/schemas")
|
||||
invalid_report = invalid_loader.load()
|
||||
|
||||
self.assertFalse(invalid_report.valid)
|
||||
self.assertEqual(
|
||||
{
|
||||
(issue.code, issue.source.file, issue.source.pointer)
|
||||
for issue in invalid_report.issues
|
||||
},
|
||||
{
|
||||
(
|
||||
"schema_error",
|
||||
"projects/bad.yaml",
|
||||
"/spec/virtualServers/0/endpointAlias",
|
||||
),
|
||||
("schema_error", "projects/bad.yaml", "/spec/capabilityPacks"),
|
||||
("schema_error", "projects/bad.yaml", "/spec"),
|
||||
("secret_value", "projects/bad.yaml", "/spec/credentialToken"),
|
||||
},
|
||||
)
|
||||
self.assertEqual(invalid_loader.store_loads, 1)
|
||||
self.assertNotIn(
|
||||
"plain-secret-value",
|
||||
json.dumps([document.payload for document in invalid_report.documents]),
|
||||
)
|
||||
|
||||
def test_committed_catalog_validates_without_network_or_secret_values(self) -> None:
|
||||
report = CatalogLoader(ROOT / "catalog").load()
|
||||
self.assertTrue(report.valid, report.issues)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts import validate_pack
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("canonical_name", "risk", "accepted"),
|
||||
[
|
||||
("server.appops.self_upgrade.apply", "R3", True),
|
||||
("server.appops.self_upgrade.apply", "R2", False),
|
||||
("server.appops.arbitrary.apply", "R3", False),
|
||||
],
|
||||
)
|
||||
def test_catalog_accepts_only_the_approved_exact_risk_write(
|
||||
monkeypatch: pytest.MonkeyPatch, canonical_name: str, risk: str, accepted: bool
|
||||
) -> None:
|
||||
tool_path = ROOT / "catalog/tools/server-appops-self-upgrade-apply.yaml"
|
||||
policy_path = next((ROOT / "catalog/policies").glob("*global-deny*"))
|
||||
tool = validate_pack.load_json(tool_path)
|
||||
tool["spec"]["canonicalName"] = canonical_name
|
||||
tool["spec"]["riskClass"] = risk
|
||||
policy = validate_pack.load_json(policy_path)
|
||||
|
||||
def read_documents(_errors: list[str]) -> list[tuple[Path, Any]]:
|
||||
return [(tool_path, tool), (policy_path, policy)]
|
||||
|
||||
monkeypatch.setattr(validate_pack, "read_catalog_documents", read_documents)
|
||||
errors: list[str] = []
|
||||
validate_pack.validate_catalog(errors, [])
|
||||
if accepted:
|
||||
assert errors == []
|
||||
else:
|
||||
assert any("invalid risk class" in error for error in errors)
|
||||
if canonical_name.endswith("arbitrary.apply"):
|
||||
assert any("not an approved controlled write tool" in error for error in errors)
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import event, select
|
||||
|
||||
from packages.hub_clients import ClientKind, ClientStatus
|
||||
from services.control_api.auth.models import AuthenticatedPrincipal, PrincipalKind
|
||||
from services.control_api.persistence.models import AuditEventRow, ClientBindingRow, ClientRow
|
||||
from services.runtime.client_service import PersistentClientService
|
||||
|
||||
pytest_plugins = ("tests.test_machine_client_provisioning",)
|
||||
|
||||
|
||||
def _actor() -> AuthenticatedPrincipal:
|
||||
now = datetime.now(UTC)
|
||||
return AuthenticatedPrincipal(
|
||||
subject="operator-audit-fixture",
|
||||
issuer="https://identity.fixture.invalid",
|
||||
audience=("hub-fixture",),
|
||||
kind=PrincipalKind.MANAGEMENT_USER,
|
||||
issued_at=now,
|
||||
expires_at=now + timedelta(minutes=5),
|
||||
)
|
||||
|
||||
|
||||
def _active_client(database: Any) -> tuple[PersistentClientService, Any]:
|
||||
service = PersistentClientService(
|
||||
database, issuer="https://identity.fixture.invalid", public_host="mcp.fixture.invalid"
|
||||
)
|
||||
record, _, _ = service.create(
|
||||
name="Audit fixture", project="ludarium", environment="prod", kind=ClientKind.STDIO_BRIDGE
|
||||
)
|
||||
with database() as session, session.begin():
|
||||
row = session.get(ClientRow, record.id)
|
||||
assert row is not None
|
||||
row.status = "active"
|
||||
binding = session.scalar(
|
||||
select(ClientBindingRow).where(ClientBindingRow.client_id == record.id)
|
||||
)
|
||||
assert binding is not None
|
||||
binding.enabled = True
|
||||
return service, record.id
|
||||
|
||||
|
||||
def test_client_disable_and_revoke_are_scoped_and_append_actor_audits(database: Any) -> None:
|
||||
service, client_id = _active_client(database)
|
||||
actor = _actor()
|
||||
disabled = service.transition(client_id, ClientStatus.DISABLED, actor)
|
||||
assert disabled.status is ClientStatus.DISABLED
|
||||
revoked = service.transition(client_id, ClientStatus.REVOKED, actor)
|
||||
assert revoked.status is ClientStatus.REVOKED
|
||||
with database() as session:
|
||||
binding = session.scalar(
|
||||
select(ClientBindingRow).where(ClientBindingRow.client_id == client_id)
|
||||
)
|
||||
assert binding is not None and binding.enabled is False
|
||||
audits = session.scalars(
|
||||
select(AuditEventRow)
|
||||
.where(AuditEventRow.client_id == client_id)
|
||||
.order_by(AuditEventRow.occurred_at)
|
||||
).all()
|
||||
assert [row.event_type for row in audits] == ["client.disable", "client.revoke"]
|
||||
for row in audits:
|
||||
assert row.principal_subject == actor.subject
|
||||
assert row.principal_issuer == actor.issuer
|
||||
assert row.project_id == binding.project_id
|
||||
assert row.virtual_server_id == binding.virtual_server_id
|
||||
assert row.environment_id is not None
|
||||
assert row.policy_decision == "allow" and row.outcome == "success"
|
||||
assert row.sanitized_arguments == {"clientId": str(client_id), "action": row.event_type}
|
||||
assert row.arguments_hash is not None and len(row.arguments_hash) == 64
|
||||
assert audits[0].trace_id != audits[1].trace_id
|
||||
with pytest.raises(ValueError, match="revoked"):
|
||||
service.transition(client_id, ClientStatus.DISABLED, actor)
|
||||
with database() as session:
|
||||
row = session.get(ClientRow, client_id)
|
||||
assert row is not None and row.status == "revoked"
|
||||
assert len(session.scalars(select(AuditEventRow)).all()) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [ClientStatus.DISABLED, ClientStatus.REVOKED])
|
||||
def test_audit_failure_rolls_back_client_and_binding_transition(
|
||||
database: Any, status: ClientStatus
|
||||
) -> None:
|
||||
service, client_id = _active_client(database)
|
||||
|
||||
def reject_audit(*_: Any) -> None:
|
||||
raise RuntimeError("fixture audit insert failed")
|
||||
|
||||
event.listen(AuditEventRow, "before_insert", reject_audit)
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="fixture audit insert failed"):
|
||||
service.transition(client_id, status, _actor())
|
||||
finally:
|
||||
event.remove(AuditEventRow, "before_insert", reject_audit)
|
||||
with database() as session:
|
||||
row = session.get(ClientRow, client_id)
|
||||
binding = session.scalar(
|
||||
select(ClientBindingRow).where(ClientBindingRow.client_id == client_id)
|
||||
)
|
||||
assert row is not None and row.status == "active"
|
||||
assert row.revoked_at is None
|
||||
assert row.record_metadata["operatorDisabled"] is False
|
||||
assert binding is not None and binding.enabled is True
|
||||
assert session.scalars(select(AuditEventRow)).all() == []
|
||||
@@ -0,0 +1,37 @@
|
||||
"""The browser harness supplies every real web readiness dependency offline."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.e2e.harness import run_web, worker_readiness_fixture
|
||||
|
||||
|
||||
def test_worker_fixture_is_explicit_and_read_only() -> None:
|
||||
with TestClient(worker_readiness_fixture()) as client:
|
||||
response = client.get("/health/ready")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ready", "component": "e2e-worker-fixture"}
|
||||
assert client.post("/health/ready").status_code == 405
|
||||
|
||||
|
||||
def test_web_harness_configures_both_upstream_dependencies(tmp_path) -> None:
|
||||
(tmp_path / "index.html").write_text("<html></html>", encoding="utf-8")
|
||||
with (
|
||||
patch("tests.e2e.harness.os.environ", {}),
|
||||
patch("tests.e2e.harness.web_runtime.build_app") as build,
|
||||
patch("tests.e2e.harness.generate_local_tls_material") as tls,
|
||||
patch("tests.e2e.harness.uvicorn.run") as serve,
|
||||
):
|
||||
import os
|
||||
|
||||
def inspect_dependencies():
|
||||
assert os.environ["HUB_CONTROL_URL"] == "http://127.0.0.1:8781"
|
||||
assert os.environ["HUB_WORKER_URL"] == "http://127.0.0.1:8783"
|
||||
return "configured-app"
|
||||
|
||||
build.side_effect = inspect_dependencies
|
||||
tls.return_value.cert_path = tmp_path / "cert.pem"
|
||||
tls.return_value.key_path = tmp_path / "key.pem"
|
||||
run_web(8780, 8781, 8783, tmp_path)
|
||||
assert serve.call_args.args == ("configured-app",)
|
||||
@@ -267,8 +267,7 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
"Jens/projectbrain",
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" - name: Validate repository with a bounded profile\n",
|
||||
" - name: Validate repository with a bounded profile\n"
|
||||
" if: false\n",
|
||||
" - name: Validate repository with a bounded profile\n if: false\n",
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -293,14 +292,11 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
bypass_mutations = (
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm test:e2e:real\n",
|
||||
" cat <<'EOF'\n"
|
||||
" pnpm test:e2e:real\n"
|
||||
" EOF\n",
|
||||
" cat <<'EOF'\n pnpm test:e2e:real\n EOF\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm --dir backend test:database\n",
|
||||
" exit 0\n"
|
||||
" pnpm --dir backend test:database\n",
|
||||
" exit 0\n pnpm --dir backend test:database\n",
|
||||
),
|
||||
PROJECTBRAIN_VALIDATION_WORKFLOW.replace(
|
||||
" pnpm test:e2e:real\n",
|
||||
@@ -357,9 +353,7 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
"Jens/projectbrain", PROJECTBRAIN_VALIDATION_WORKFLOW + "\njobs: {}\n"
|
||||
)
|
||||
)
|
||||
self.assertIsNone(
|
||||
_managed_validation_profile("Acme/hub", PROJECTBRAIN_VALIDATION_WORKFLOW)
|
||||
)
|
||||
self.assertIsNone(_managed_validation_profile("Acme/hub", PROJECTBRAIN_VALIDATION_WORKFLOW))
|
||||
self.assertEqual(
|
||||
_managed_validation_profile("Acme/hub", MANAGED_VALIDATION_WORKFLOW),
|
||||
"canonical",
|
||||
@@ -1400,9 +1394,7 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"] = [
|
||||
"Pull request / quality (pull_request)"
|
||||
]
|
||||
original_protection = copy.deepcopy(
|
||||
fixture._branch_protections["Jens/projectbrain"][0]
|
||||
)
|
||||
original_protection = copy.deepcopy(fixture._branch_protections["Jens/projectbrain"][0])
|
||||
try:
|
||||
current = context()
|
||||
preview = connector_instance.branch_protection_preview(
|
||||
@@ -1467,9 +1459,9 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
nonlocal read_count
|
||||
read_count += 1
|
||||
if read_count == 3:
|
||||
fixture._branch_protections["Jens/projectbrain"][0][
|
||||
"status_check_contexts"
|
||||
].remove("Additional required check")
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"].remove(
|
||||
"Additional required check"
|
||||
)
|
||||
return original_read(repository)
|
||||
|
||||
try:
|
||||
@@ -1704,9 +1696,9 @@ class GiteaConnectorTests(unittest.TestCase):
|
||||
nonlocal read_count
|
||||
read_count += 1
|
||||
if read_count == 3:
|
||||
fixture._branch_protections["Jens/projectbrain"][0][
|
||||
"status_check_contexts"
|
||||
].remove("Additional required check")
|
||||
fixture._branch_protections["Jens/projectbrain"][0]["status_check_contexts"].remove(
|
||||
"Additional required check"
|
||||
)
|
||||
return original_read(repository)
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.hub_clients import ClientKind, generate_config
|
||||
from services.control_api.persistence.db import Base, create_engine_from_url, session_factory
|
||||
from services.control_api.persistence.machine_client import provision_machine_client
|
||||
from services.control_api.persistence.models import (
|
||||
@@ -14,6 +15,7 @@ from services.control_api.persistence.models import (
|
||||
VirtualServerRow,
|
||||
WorkspaceRow,
|
||||
)
|
||||
from services.runtime.client_service import PersistentClientService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -114,8 +116,19 @@ def test_revoked_machine_client_cannot_be_silently_reactivated(database) -> None
|
||||
_provision(database)
|
||||
|
||||
|
||||
def test_pending_ui_client_is_activated_in_place(database) -> None: # type: ignore[no-untyped-def]
|
||||
@pytest.mark.parametrize("profile", [*ClientKind, None])
|
||||
def test_pending_ui_client_is_activated_in_place_with_selected_profile(
|
||||
database, profile: ClientKind | None
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
pending_id = uuid4()
|
||||
expected_profile = profile or ClientKind.STDIO_BRIDGE
|
||||
metadata = {
|
||||
"managedBy": "runtime-onboarding",
|
||||
"project": "ludarium",
|
||||
"environment": "prod",
|
||||
}
|
||||
if profile is not None:
|
||||
metadata["kind"] = profile.value
|
||||
with database() as session, session.begin():
|
||||
workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one()
|
||||
project = session.query(ProjectRow).filter_by(slug="ludarium").one()
|
||||
@@ -127,11 +140,7 @@ def test_pending_ui_client_is_activated_in_place(database) -> None: # type: ign
|
||||
subject=f"pending-{pending_id}",
|
||||
issuer="https://auth.itworx.test/application/o/itworx-mcp-hub/",
|
||||
status="pending",
|
||||
record_metadata={
|
||||
"managedBy": "runtime-onboarding",
|
||||
"project": "ludarium",
|
||||
"environment": "prod",
|
||||
},
|
||||
record_metadata=metadata,
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
@@ -162,3 +171,55 @@ def test_pending_ui_client_is_activated_in_place(database) -> None: # type: ign
|
||||
assert client.status == "active"
|
||||
assert client.subject == "b7f5d1aa-f199-4334-b077-f6be0870115f"
|
||||
assert binding.enabled is True
|
||||
assert client.record_metadata["kind"] == expected_profile.value
|
||||
assert generate_config(PersistentClientService._record(client)).kind is expected_profile
|
||||
|
||||
|
||||
def test_pending_ui_client_rejects_unknown_profile(database) -> None: # type: ignore[no-untyped-def]
|
||||
pending_id = uuid4()
|
||||
with database() as session, session.begin():
|
||||
workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one()
|
||||
project = session.query(ProjectRow).filter_by(slug="ludarium").one()
|
||||
server = session.query(VirtualServerRow).filter_by(slug="ludarium-prod-readonly").one()
|
||||
session.add(
|
||||
ClientRow(
|
||||
id=pending_id,
|
||||
workspace_id=workspace.id,
|
||||
subject=f"pending-{pending_id}",
|
||||
issuer="https://auth.itworx.test/application/o/itworx-mcp-hub/",
|
||||
status="pending",
|
||||
record_metadata={
|
||||
"managedBy": "runtime-onboarding",
|
||||
"project": "ludarium",
|
||||
"environment": "prod",
|
||||
"kind": "arbitrary-profile",
|
||||
},
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
session.add(
|
||||
ClientBindingRow(
|
||||
workspace_id=workspace.id,
|
||||
project_id=project.id,
|
||||
client_id=pending_id,
|
||||
virtual_server_id=server.id,
|
||||
enabled=False,
|
||||
)
|
||||
)
|
||||
with database() as session:
|
||||
with pytest.raises(ValueError, match="invalid client kind"):
|
||||
with session.begin():
|
||||
provision_machine_client(
|
||||
session,
|
||||
issuer="https://auth.itworx.test/application/o/itworx-mcp-hub/",
|
||||
subject="b7f5d1aa-f199-4334-b077-f6be0870115f",
|
||||
oauth_username="svc-mcp-codex-ludarium",
|
||||
name="Codex workstation",
|
||||
project_slug="ludarium",
|
||||
client_id=pending_id,
|
||||
)
|
||||
with database() as session:
|
||||
client = session.get(ClientRow, pending_id)
|
||||
assert client is not None
|
||||
assert client.status == "pending"
|
||||
assert client.record_metadata["kind"] == "arbitrary-profile"
|
||||
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from services.control_api.persistence import create_engine_from_url, session_factory
|
||||
from services.control_api.persistence.models import (
|
||||
AuditEventRow,
|
||||
EnvironmentRow,
|
||||
ProjectRow,
|
||||
WorkspaceRow,
|
||||
)
|
||||
from tests.test_wp230_rbac import ManagementRole, RoleSession
|
||||
|
||||
pytest_plugins = ("tests.test_wp230_rbac",)
|
||||
|
||||
|
||||
def test_resource_search_matches_connector_slug(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
response = role_sessions[ManagementRole.VIEWER].get(
|
||||
"/api/connectors", params={"q": "knowledge-default"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert [row["id"] for row in body["rows"]] == ["knowledge-default"]
|
||||
|
||||
|
||||
def test_project_scoped_connector_search_uses_bindings_and_environment(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
viewer = role_sessions[ManagementRole.VIEWER]
|
||||
response = viewer.get(
|
||||
"/api/connectors",
|
||||
params={
|
||||
"project": "ludarium",
|
||||
"environment": "prod",
|
||||
"q": "knowledge-default",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
rows = response.json()["rows"]
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["id"] == "knowledge-default"
|
||||
assert rows[0]["project"] == "ludarium"
|
||||
assert rows[0]["environment"] == "prod"
|
||||
assert not any("url" in key.casefold() for key in rows[0])
|
||||
|
||||
unbound_project = viewer.get(
|
||||
"/api/connectors",
|
||||
params={
|
||||
"project": "mobilityops",
|
||||
"environment": "prod",
|
||||
"q": "knowledge-default",
|
||||
},
|
||||
)
|
||||
assert unbound_project.status_code == 200
|
||||
assert unbound_project.json()["rows"] == []
|
||||
|
||||
|
||||
def test_resource_search_matches_tool_canonical_name(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
response = role_sessions[ManagementRole.VIEWER].get(
|
||||
"/api/tools", params={"q": "deployment.project.status"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert [row["canonicalName"] for row in body["rows"]] == ["deployment.project.status"]
|
||||
|
||||
|
||||
def test_project_scoped_tool_search_uses_bindings_and_capability_packs(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
viewer = role_sessions[ManagementRole.VIEWER]
|
||||
|
||||
ludarium_tools = viewer.get(
|
||||
"/api/tools",
|
||||
params={"project": "ludarium", "limit": 100},
|
||||
)
|
||||
assert ludarium_tools.status_code == 200
|
||||
ludarium_body = ludarium_tools.json()
|
||||
ludarium_rows = ludarium_body["rows"]
|
||||
assert ludarium_body["page"]["total"] == len(ludarium_rows)
|
||||
ludarium_names = {row["canonicalName"] for row in ludarium_rows}
|
||||
assert "deployment.project.status" in ludarium_names
|
||||
assert "gitea.branch.delete" not in ludarium_names
|
||||
assert all(row["project"] == "ludarium" for row in ludarium_rows)
|
||||
assert all(row["environment"] == "prod" for row in ludarium_rows)
|
||||
|
||||
unbound_connector = viewer.get(
|
||||
"/api/tools",
|
||||
params={
|
||||
"project": "mobilityops",
|
||||
"environment": "prod",
|
||||
"q": "deployment.project.status",
|
||||
},
|
||||
)
|
||||
assert unbound_connector.status_code == 200
|
||||
assert unbound_connector.json()["rows"] == []
|
||||
|
||||
environment_without_server = viewer.get(
|
||||
"/api/tools",
|
||||
params={
|
||||
"project": "forgeflow",
|
||||
"environment": "dev",
|
||||
"q": "deployment.project.status",
|
||||
},
|
||||
)
|
||||
assert environment_without_server.status_code == 200
|
||||
assert environment_without_server.json()["rows"] == []
|
||||
|
||||
|
||||
def test_audit_search_matches_trace_id_without_exposing_search_to_viewers(
|
||||
role_sessions: dict[ManagementRole, RoleSession], db_path: Path
|
||||
) -> None:
|
||||
trace_id = f"operator-search-{uuid4()}"
|
||||
engine = create_engine_from_url(f"sqlite:///{db_path}")
|
||||
sessions = session_factory(engine)
|
||||
with sessions() as session, session.begin():
|
||||
workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one()
|
||||
project = session.query(ProjectRow).filter_by(slug="ludarium").one()
|
||||
environment = (
|
||||
session.query(EnvironmentRow).filter_by(project_id=project.id, name="prod").one()
|
||||
)
|
||||
session.add(
|
||||
AuditEventRow(
|
||||
trace_id=trace_id,
|
||||
workspace_id=workspace.id,
|
||||
project_id=project.id,
|
||||
environment_id=environment.id,
|
||||
event_type="operator.search.fixture",
|
||||
tool_name="operator.search.fixture",
|
||||
policy_decision="must-not-be-a-search-field",
|
||||
outcome="success",
|
||||
)
|
||||
)
|
||||
engine.dispose()
|
||||
|
||||
auditor = role_sessions[ManagementRole.AUDITOR]
|
||||
matching = auditor.get("/api/audit", params={"q": trace_id})
|
||||
assert matching.status_code == 200
|
||||
assert [row["traceId"] for row in matching.json()["rows"]] == [trace_id]
|
||||
|
||||
excluded_field = auditor.get("/api/audit", params={"q": "must-not-be-a-search-field"})
|
||||
assert excluded_field.status_code == 200
|
||||
assert excluded_field.json()["rows"] == []
|
||||
|
||||
denied = role_sessions[ManagementRole.VIEWER].get("/api/audit", params={"q": trace_id})
|
||||
assert denied.status_code == 403
|
||||
|
||||
|
||||
def test_servers_list_virtual_server_identities_and_apply_project_environment_scope(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
viewer = role_sessions[ManagementRole.VIEWER]
|
||||
|
||||
scoped = viewer.get(
|
||||
"/api/servers",
|
||||
params={"project": "forgeflow", "environment": "prod", "limit": 1},
|
||||
)
|
||||
assert scoped.status_code == 200
|
||||
scoped_body = scoped.json()
|
||||
assert scoped_body["page"]["total"] == 1
|
||||
assert scoped_body["rows"] == [
|
||||
{
|
||||
"id": "forgeflow-prod-readonly",
|
||||
"name": "forgeflow-prod-readonly",
|
||||
"status": "unknown",
|
||||
"summary": "Catalog registered; live runtime health has not been observed.",
|
||||
"project": "forgeflow",
|
||||
"environment": "prod",
|
||||
"risk": None,
|
||||
"canonicalName": None,
|
||||
}
|
||||
]
|
||||
|
||||
wrong_environment = viewer.get(
|
||||
"/api/servers", params={"project": "forgeflow", "environment": "dev"}
|
||||
)
|
||||
assert wrong_environment.status_code == 200
|
||||
assert wrong_environment.json()["page"]["total"] == 0
|
||||
assert wrong_environment.json()["rows"] == []
|
||||
|
||||
wrong_project = viewer.get(
|
||||
"/api/servers",
|
||||
params={"project": "mobilityops", "environment": "prod", "q": "forgeflow-prod-readonly"},
|
||||
)
|
||||
assert wrong_project.status_code == 200
|
||||
assert wrong_project.json()["page"]["total"] == 0
|
||||
assert wrong_project.json()["rows"] == []
|
||||
@@ -0,0 +1,519 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from services.control_api.auth.config import OIDCSettings
|
||||
from services.control_api.auth.fake_provider import FakeOIDCProvider
|
||||
from services.control_api.auth.models import (
|
||||
AuthenticatedPrincipal,
|
||||
ManagementRole,
|
||||
PrincipalKind,
|
||||
)
|
||||
from services.control_api.contextforge.fake import FakeContextForge
|
||||
from services.control_api.contextforge.models import ContextForgeSettings
|
||||
from services.control_api.persistence import Base, create_engine_from_url, session_factory
|
||||
from services.control_api.persistence.machine_client import provision_machine_client
|
||||
from services.control_api.persistence.models import (
|
||||
AuditEventRow,
|
||||
ClientBindingRow,
|
||||
ClientRow,
|
||||
ConnectorRow,
|
||||
EnvironmentRow,
|
||||
ProjectRow,
|
||||
VirtualServerRow,
|
||||
WorkspaceRow,
|
||||
)
|
||||
from services.runtime import control_api
|
||||
from services.runtime.control_api import build_app
|
||||
from services.runtime.state_sync import sync_runtime_state
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ISSUER = "https://fake.operator-workspaces.test"
|
||||
ROLE_GROUPS = {
|
||||
ManagementRole.VIEWER: "hub-viewer",
|
||||
ManagementRole.OPERATOR: "hub-operator",
|
||||
ManagementRole.ADMIN: "hub-admin",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoleSession:
|
||||
app: Any
|
||||
client: TestClient
|
||||
csrf: str
|
||||
|
||||
def get(self, path: str) -> Any:
|
||||
return self.client.get(path)
|
||||
|
||||
def post(self, path: str, **kwargs: Any) -> Any:
|
||||
headers = dict(kwargs.pop("headers", {}) or {})
|
||||
headers["X-CSRF-Token"] = self.csrf
|
||||
return self.client.post(path, headers=headers, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def client_bindings_env() -> Iterator[None]:
|
||||
previous_bindings = os.environ.get("MCP_CLIENT_BINDINGS_JSON")
|
||||
previous_token = os.environ.get("OPERATOR_WORKSPACES_FAKE_CONTEXTFORGE_TOKEN")
|
||||
os.environ["MCP_CLIENT_BINDINGS_JSON"] = "[]"
|
||||
os.environ["OPERATOR_WORKSPACES_FAKE_CONTEXTFORGE_TOKEN"] = "operator-workspaces-fake-token" # noqa: S105
|
||||
yield
|
||||
if previous_bindings is None:
|
||||
os.environ.pop("MCP_CLIENT_BINDINGS_JSON", None)
|
||||
else:
|
||||
os.environ["MCP_CLIENT_BINDINGS_JSON"] = previous_bindings
|
||||
if previous_token is None:
|
||||
os.environ.pop("OPERATOR_WORKSPACES_FAKE_CONTEXTFORGE_TOKEN", None)
|
||||
else:
|
||||
os.environ["OPERATOR_WORKSPACES_FAKE_CONTEXTFORGE_TOKEN"] = previous_token
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def db_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
path = tmp_path_factory.mktemp("operator-workspaces") / "hub.db"
|
||||
engine = create_engine_from_url(f"sqlite:///{path}")
|
||||
Base.metadata.create_all(engine)
|
||||
sessions = session_factory(engine)
|
||||
with sessions() as session, session.begin():
|
||||
sync_runtime_state(session, ROOT / "catalog", ISSUER)
|
||||
engine.dispose()
|
||||
return path
|
||||
|
||||
|
||||
def _login(client: TestClient, provider: FakeOIDCProvider) -> str:
|
||||
login = client.get("/auth/login", follow_redirects=False)
|
||||
query = parse_qs(urlparse(login.headers["location"]).query)
|
||||
code = provider.authorize(login.headers["location"])
|
||||
callback = client.get(
|
||||
"/auth/callback",
|
||||
params={"code": code, "state": query["state"][0]},
|
||||
follow_redirects=False,
|
||||
)
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(callback.headers["set-cookie"])
|
||||
cookie_name = next(iter(cookie))
|
||||
client.cookies.set(cookie_name, cookie[cookie_name].value)
|
||||
return str(client.get("/auth/session").json()["csrf_token"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def role_sessions(db_path: Path) -> Iterator[dict[ManagementRole, RoleSession]]:
|
||||
with ExitStack() as stack:
|
||||
result: dict[ManagementRole, RoleSession] = {}
|
||||
for role, group in ROLE_GROUPS.items():
|
||||
provider = FakeOIDCProvider(issuer=ISSUER, user_groups=(group,))
|
||||
oidc = OIDCSettings(
|
||||
issuer=ISSUER,
|
||||
audience=provider.audience,
|
||||
client_id="operator-workspaces-hub",
|
||||
redirect_uri="https://hub.test/auth/callback",
|
||||
secure_cookie=True,
|
||||
role_group_map={value: key for key, value in ROLE_GROUPS.items()},
|
||||
)
|
||||
engine = create_engine_from_url(f"sqlite:///{db_path}")
|
||||
app = build_app(
|
||||
oidc=oidc,
|
||||
provider_transport=provider.transport(),
|
||||
contextforge_settings=ContextForgeSettings(
|
||||
base_url="https://contextforge.test",
|
||||
auth_token_env="OPERATOR_WORKSPACES_FAKE_CONTEXTFORGE_TOKEN", # noqa: S106
|
||||
),
|
||||
contextforge_transport=FakeContextForge().transport(),
|
||||
engine=engine,
|
||||
catalog_root=ROOT / "catalog",
|
||||
)
|
||||
client = stack.enter_context(TestClient(app))
|
||||
csrf = _login(client, provider)
|
||||
result[role] = RoleSession(app=app, client=client, csrf=csrf)
|
||||
yield result
|
||||
|
||||
|
||||
def _ready_probe(_: str) -> dict[str, Any]:
|
||||
return {"reachable": True, "status": "ready", "detail": "Direct connector readiness verified."}
|
||||
|
||||
|
||||
def test_project_workspace_returns_registered_server_and_source_metadata(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
response = role_sessions[ManagementRole.VIEWER].get("/api/projects/ludarium")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["mcpServers"]
|
||||
assert body["mcpServers"][0]["endpointAlias"] == "/mcp/ludarium/prod"
|
||||
assert body["mcpServers"][0]["stateSource"] in {"persisted", "catalog-only"}
|
||||
assert body["knowledgeSources"]
|
||||
source = body["knowledgeSources"][0]
|
||||
assert source["uri"] == "discovery://repository/ludarium"
|
||||
assert source["indexesSecrets"] is False
|
||||
assert "scoped Knowledge connector" in source["accessBoundary"]
|
||||
assert "README*" in source["include"]
|
||||
assert "credentialReferences" not in response.text
|
||||
|
||||
|
||||
def test_connector_workspace_is_scoped_and_secret_free(
|
||||
role_sessions: dict[ManagementRole, RoleSession], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
probe_calls: list[str] = []
|
||||
|
||||
def record_probe(slug: str) -> dict[str, Any]:
|
||||
probe_calls.append(slug)
|
||||
return _ready_probe(slug)
|
||||
|
||||
monkeypatch.setattr(control_api, "_direct_connector_readiness", record_probe)
|
||||
viewer = role_sessions[ManagementRole.VIEWER]
|
||||
response = viewer.get("/api/connectors/knowledge-default?project=ludarium&environment=prod")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["id"] == "knowledge-default"
|
||||
assert body["connectorType"] == "knowledge"
|
||||
assert body["registration"]["state"] == "registered"
|
||||
assert body["readiness"]["source"] in {"persisted", "unavailable"}
|
||||
if body["readiness"]["source"] == "unavailable":
|
||||
assert body["readiness"]["observedAt"] is None
|
||||
assert probe_calls == []
|
||||
assert body["bindings"] == [
|
||||
{"project": "ludarium", "environment": "prod", "verification": "verified"}
|
||||
]
|
||||
assert body["relatedTools"]
|
||||
for sensitive_key in ("credentialReferences", "credentialReference", "endpoint", "token"):
|
||||
assert sensitive_key not in body
|
||||
assert "credentialReferences" not in response.text
|
||||
assert "service_token" not in response.text
|
||||
|
||||
assert (
|
||||
viewer.get("/api/connectors/knowledge-default?project=missing&environment=prod").status_code
|
||||
== 404
|
||||
)
|
||||
assert (
|
||||
viewer.get(
|
||||
"/api/connectors/knowledge-default?project=ludarium&environment=staging"
|
||||
).status_code
|
||||
== 404
|
||||
)
|
||||
assert viewer.get("/api/connectors/knowledge-default?project=ludarium").status_code == 422
|
||||
|
||||
|
||||
def test_connector_workspace_marks_old_persisted_health_unknown_without_probing(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
db_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
probe_calls: list[str] = []
|
||||
|
||||
def record_probe(slug: str) -> dict[str, Any]:
|
||||
probe_calls.append(slug)
|
||||
return _ready_probe(slug)
|
||||
|
||||
monkeypatch.setattr(control_api, "_direct_connector_readiness", record_probe)
|
||||
engine = create_engine_from_url(f"sqlite:///{db_path}")
|
||||
sessions = session_factory(engine)
|
||||
observed_at = datetime.now(UTC) - timedelta(minutes=5)
|
||||
with sessions() as session, session.begin():
|
||||
workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one()
|
||||
project = (
|
||||
session.query(ProjectRow).filter_by(workspace_id=workspace.id, slug="ludarium").one()
|
||||
)
|
||||
environment = (
|
||||
session.query(EnvironmentRow)
|
||||
.filter_by(workspace_id=workspace.id, project_id=project.id, name="prod")
|
||||
.one()
|
||||
)
|
||||
connector = (
|
||||
session.query(ConnectorRow)
|
||||
.filter_by(
|
||||
workspace_id=workspace.id,
|
||||
project_id=project.id,
|
||||
environment_scope="prod",
|
||||
type="knowledge",
|
||||
slug="knowledge-default",
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if connector is None:
|
||||
connector = ConnectorRow(
|
||||
id=uuid4(),
|
||||
workspace_id=workspace.id,
|
||||
project_id=project.id,
|
||||
environment_id=environment.id,
|
||||
environment_scope="prod",
|
||||
type="knowledge",
|
||||
slug="knowledge-default",
|
||||
)
|
||||
session.add(connector)
|
||||
connector.health_state = "healthy"
|
||||
connector.last_check_at = observed_at
|
||||
connector.endpoint = "https://knowledge.example.invalid/health"
|
||||
connector.credential_reference = "vault://private-reference"
|
||||
engine.dispose()
|
||||
|
||||
response = role_sessions[ManagementRole.VIEWER].get(
|
||||
"/api/connectors/knowledge-default?project=ludarium&environment=prod"
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
readiness = response.json()["readiness"]
|
||||
assert readiness["source"] == "persisted"
|
||||
assert readiness["stale"] is True
|
||||
assert readiness["reachable"] is None
|
||||
assert readiness["status"] == "unknown"
|
||||
assert readiness["observedAt"]
|
||||
assert "Stale observation" in readiness["detail"]
|
||||
assert "knowledge.example.invalid" not in response.text
|
||||
assert "vault://private-reference" not in response.text
|
||||
assert probe_calls == []
|
||||
|
||||
|
||||
def test_connector_and_onboarding_routes_deny_machine_principals_even_with_admin_role(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
app = role_sessions[ManagementRole.VIEWER].app
|
||||
route = next(
|
||||
route
|
||||
for route in app.routes
|
||||
if isinstance(route, APIRoute) and route.path == "/api/connectors/{slug}"
|
||||
)
|
||||
dependency_calls: list[Any] = []
|
||||
|
||||
def visit(dependant: Any) -> None:
|
||||
if dependant.call is not None:
|
||||
dependency_calls.append(dependant.call)
|
||||
for child in dependant.dependencies:
|
||||
visit(child)
|
||||
|
||||
visit(route.dependant)
|
||||
authenticate = next(
|
||||
call for call in dependency_calls if getattr(call, "__name__", "") == "authenticate"
|
||||
)
|
||||
now = datetime.now(UTC)
|
||||
machine = AuthenticatedPrincipal(
|
||||
subject="machine-connector-reader",
|
||||
issuer=ISSUER,
|
||||
audience=("operator-workspaces-hub",),
|
||||
kind=PrincipalKind.MACHINE_CLIENT,
|
||||
client_id=uuid4(),
|
||||
roles=frozenset({ManagementRole.ADMIN}),
|
||||
expires_at=now + timedelta(minutes=5),
|
||||
issued_at=now,
|
||||
)
|
||||
app.dependency_overrides[authenticate] = lambda: machine
|
||||
try:
|
||||
client = role_sessions[ManagementRole.VIEWER].client
|
||||
connector = client.get("/api/connectors/knowledge-default")
|
||||
onboarding = client.get(f"/api/client-management/{uuid4()}/onboarding-status")
|
||||
finally:
|
||||
app.dependency_overrides.pop(authenticate, None)
|
||||
assert connector.status_code == 403
|
||||
assert onboarding.status_code == 403
|
||||
|
||||
|
||||
def test_onboarding_verification_records_server_checks_without_enabling_client_or_binding(
|
||||
role_sessions: dict[ManagementRole, RoleSession], db_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
probe_calls: list[str] = []
|
||||
|
||||
def record_probe(slug: str) -> dict[str, Any]:
|
||||
probe_calls.append(slug)
|
||||
return _ready_probe(slug)
|
||||
|
||||
monkeypatch.setattr(control_api, "_direct_connector_readiness", record_probe)
|
||||
admin = role_sessions[ManagementRole.ADMIN]
|
||||
operator = role_sessions[ManagementRole.OPERATOR]
|
||||
viewer = role_sessions[ManagementRole.VIEWER]
|
||||
created = admin.post(
|
||||
"/api/client-management",
|
||||
json={
|
||||
"name": "resume-me",
|
||||
"project": "gitea-loop",
|
||||
"environment": "prod",
|
||||
"kind": "native-2026",
|
||||
},
|
||||
)
|
||||
assert created.status_code == 201, created.text
|
||||
client_id = created.json()["client"]["id"]
|
||||
|
||||
operator_status = operator.get(f"/api/client-management/{client_id}/onboarding-status")
|
||||
assert operator_status.status_code == 200, operator_status.text
|
||||
assert operator_status.json()["checks"]["connector"]["state"] == "unknown"
|
||||
assert probe_calls == []
|
||||
|
||||
status = viewer.get(f"/api/client-management/{client_id}/onboarding-status")
|
||||
assert status.status_code == 200, status.text
|
||||
before = status.json()
|
||||
assert before["clientStatus"] == "pending"
|
||||
assert before["checks"]["externalSubject"]["state"] == "pending"
|
||||
assert before["checks"]["externalScope"]["state"] == "unavailable"
|
||||
assert before["checks"]["catalog"]["state"] == "ready"
|
||||
assert before["checks"]["hubBinding"]["state"] == "pending"
|
||||
assert before["checks"]["connector"]["state"] == "unknown"
|
||||
assert before["checks"]["connector"]["observedAt"] is None
|
||||
assert before["checks"]["mcpProof"]["state"] == "unavailable"
|
||||
handoff = before["provisioning"]
|
||||
assert handoff == {
|
||||
"status": "pending-authentik-service-account",
|
||||
"subject": None,
|
||||
"issuer": ISSUER,
|
||||
"oauthUsername": f"svc-mcp-codex-gitea-loop-{client_id.replace('-', '')[:8]}",
|
||||
"tokenEndpoint": f"{ISSUER}/application/o/token/",
|
||||
"oauthClientId": "operator-workspaces-hub",
|
||||
"steps": [
|
||||
"Provision the named Authentik service account and app password.",
|
||||
"Activate this pending Hub client with the exact Authentik user UUID.",
|
||||
"Install the generated Codex stdio profile using a user-only credential file.",
|
||||
"Run MCP discovery, one read-only invocation and one cross-project denial.",
|
||||
],
|
||||
}
|
||||
assert not {"credentialReference", "bootstrapReference", "credential", "secret"} & set(handoff)
|
||||
assert "credentialReference" not in status.text
|
||||
assert "bootstrapReference" not in status.text
|
||||
assert "authentik-bootstrap://" not in status.text
|
||||
assert "authentik://service-account/" not in status.text
|
||||
assert probe_calls == []
|
||||
denied_verify = viewer.post(f"/api/client-management/{client_id}/verify")
|
||||
assert denied_verify.status_code == 403
|
||||
assert probe_calls == []
|
||||
|
||||
checked_pending = admin.post(f"/api/client-management/{client_id}/verify")
|
||||
assert checked_pending.status_code == 200, checked_pending.text
|
||||
assert probe_calls
|
||||
pending_verification_probe_count = len(probe_calls)
|
||||
assert checked_pending.json()["verification"]["checkedByManagement"] is True
|
||||
assert checked_pending.json()["verification"]["verifiedByManagement"] is False
|
||||
assert checked_pending.json()["clientStatus"] == "pending"
|
||||
|
||||
engine = create_engine_from_url(f"sqlite:///{db_path}")
|
||||
sessions = session_factory(engine)
|
||||
with sessions() as session, session.begin():
|
||||
server = session.query(VirtualServerRow).filter_by(slug="gitea-loop-prod").one()
|
||||
server.gateway_external_id = "fixture-contextforge-server"
|
||||
with sessions() as session, session.begin():
|
||||
provision_machine_client(
|
||||
session,
|
||||
issuer=ISSUER,
|
||||
subject="authentik-user-uuid-1234",
|
||||
oauth_username="svc-mcp-codex-gitea-loop-test1234",
|
||||
name="resume-me",
|
||||
project_slug="gitea-loop",
|
||||
environment_name="prod",
|
||||
public_host="hub.test",
|
||||
client_id=UUID(client_id),
|
||||
)
|
||||
|
||||
resumed = viewer.get(f"/api/client-management/{client_id}/onboarding-status")
|
||||
assert resumed.status_code == 200, resumed.text
|
||||
resumed_body = resumed.json()
|
||||
assert resumed_body["clientStatus"] == "enabled"
|
||||
assert resumed_body["checks"]["externalSubject"]["state"] == "ready"
|
||||
assert resumed_body["checks"]["hubBinding"]["state"] == "ready"
|
||||
assert resumed_body["provisioning"]["status"] == "authentik-service-account-provisioned"
|
||||
assert resumed_body["provisioning"]["subject"] == "authentik-user-uuid-1234"
|
||||
assert resumed_body["provisioning"]["oauthUsername"] == "svc-mcp-codex-gitea-loop-test1234"
|
||||
assert resumed_body["provisioning"]["issuer"] == ISSUER
|
||||
assert resumed_body["provisioning"]["tokenEndpoint"] == f"{ISSUER}/application/o/token/"
|
||||
assert resumed_body["provisioning"]["oauthClientId"] == "operator-workspaces-hub"
|
||||
assert len(probe_calls) == pending_verification_probe_count
|
||||
assert resumed_body["checks"]["externalScope"]["state"] == "unavailable"
|
||||
assert resumed_body["checks"]["mcpProof"]["state"] == "unavailable"
|
||||
|
||||
with sessions() as session, session.begin():
|
||||
client_row = session.get(ClientRow, UUID(client_id))
|
||||
assert client_row is not None
|
||||
client_row.issuer = "https://foreign-identity.example.invalid"
|
||||
probes_before_foreign_issuer_read = len(probe_calls)
|
||||
foreign_issuer_status = viewer.get(f"/api/client-management/{client_id}/onboarding-status")
|
||||
assert foreign_issuer_status.status_code == 200, foreign_issuer_status.text
|
||||
foreign_issuer_body = foreign_issuer_status.json()
|
||||
assert foreign_issuer_body["checks"]["externalSubject"]["state"] == "pending"
|
||||
assert foreign_issuer_body["provisioning"]["subject"] is None
|
||||
assert foreign_issuer_body["verification"]["verifiedByManagement"] is False
|
||||
assert len(probe_calls) == probes_before_foreign_issuer_read
|
||||
|
||||
foreign_issuer_verification = admin.post(f"/api/client-management/{client_id}/verify")
|
||||
assert foreign_issuer_verification.status_code == 200, foreign_issuer_verification.text
|
||||
assert foreign_issuer_verification.json()["checks"]["externalSubject"]["state"] == "pending"
|
||||
assert foreign_issuer_verification.json()["verification"]["verifiedByManagement"] is False
|
||||
assert len(probe_calls) > probes_before_foreign_issuer_read
|
||||
with sessions() as session, session.begin():
|
||||
client_row = session.get(ClientRow, UUID(client_id))
|
||||
assert client_row is not None
|
||||
client_row.issuer = ISSUER
|
||||
|
||||
verified = admin.post(f"/api/client-management/{client_id}/verify")
|
||||
assert verified.status_code == 200, verified.text
|
||||
assert len(probe_calls) > pending_verification_probe_count
|
||||
after = verified.json()
|
||||
assert after["verification"]["checkedByManagement"] is True
|
||||
assert after["verification"]["verifiedByManagement"] is True
|
||||
assert after["verification"]["hubChecksPassed"] is True
|
||||
assert after["checks"]["externalScope"]["state"] == "unavailable"
|
||||
assert after["checks"]["mcpProof"]["state"] == "unavailable"
|
||||
verified_probe_count = len(probe_calls)
|
||||
persisted_status = viewer.get(f"/api/client-management/{client_id}/onboarding-status")
|
||||
assert persisted_status.status_code == 200, persisted_status.text
|
||||
assert persisted_status.json()["checks"]["connector"]["state"] == "healthy"
|
||||
assert persisted_status.json()["verification"]["verifiedByManagement"] is True
|
||||
assert len(probe_calls) == verified_probe_count
|
||||
|
||||
for stale_checked_at in (
|
||||
datetime.now(UTC) - timedelta(minutes=5),
|
||||
datetime.now(UTC) + timedelta(minutes=5),
|
||||
):
|
||||
stale_timestamp = stale_checked_at.isoformat()
|
||||
with sessions() as session, session.begin():
|
||||
client_row = session.get(ClientRow, UUID(client_id))
|
||||
assert client_row is not None
|
||||
metadata = dict(client_row.record_metadata or {})
|
||||
verification = dict(metadata["onboardingVerification"])
|
||||
verification["checkedAt"] = stale_timestamp
|
||||
metadata["onboardingVerification"] = verification
|
||||
client_row.record_metadata = metadata
|
||||
stale_status = viewer.get(f"/api/client-management/{client_id}/onboarding-status")
|
||||
assert stale_status.status_code == 200, stale_status.text
|
||||
stale_body = stale_status.json()
|
||||
assert stale_body["checks"]["connector"]["state"] == "unknown"
|
||||
assert stale_body["checks"]["connector"]["observedAt"] is None
|
||||
assert stale_body["verification"]["checkedAt"] == stale_timestamp
|
||||
assert stale_body["verification"]["checkedByManagement"] is True
|
||||
assert stale_body["verification"]["verifiedByManagement"] is False
|
||||
assert stale_body["verification"]["hubChecksPassed"] is False
|
||||
assert len(probe_calls) == verified_probe_count
|
||||
|
||||
with sessions() as session:
|
||||
client_row = session.get(ClientRow, UUID(client_id))
|
||||
workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one()
|
||||
binding = session.query(ClientBindingRow).filter_by(client_id=UUID(client_id)).one()
|
||||
audit_event = (
|
||||
session.query(AuditEventRow)
|
||||
.filter_by(client_id=UUID(client_id), event_type="client.onboarding.verify")
|
||||
.order_by(AuditEventRow.occurred_at.desc())
|
||||
.first()
|
||||
)
|
||||
engine.dispose()
|
||||
assert client_row is not None and client_row.status == "active"
|
||||
assert binding.workspace_id == workspace.id
|
||||
assert binding.enabled is True
|
||||
assert client_row.record_metadata["onboardingVerification"]["verifiedByManagement"] is True
|
||||
assert "credential" not in str(client_row.record_metadata["onboardingVerification"]).lower()
|
||||
assert audit_event is not None
|
||||
assert audit_event.principal_subject == "user-management-1"
|
||||
assert audit_event.sanitized_arguments["hubChecksPassed"] is True
|
||||
|
||||
|
||||
def test_unauthed_client_is_not_exposed_by_connector_management_route(
|
||||
role_sessions: dict[ManagementRole, RoleSession],
|
||||
) -> None:
|
||||
unauthenticated = TestClient(role_sessions[ManagementRole.VIEWER].app)
|
||||
unauthenticated.cookies.clear()
|
||||
response = unauthenticated.get("/api/connectors/knowledge-default")
|
||||
assert response.status_code == 401
|
||||
@@ -47,19 +47,29 @@ def invocation(
|
||||
class WP110GovernanceTests(unittest.TestCase):
|
||||
def test_merge_result_is_retained_in_append_only_audit(self) -> None:
|
||||
audit = InMemoryAuditLog()
|
||||
engine = PolicyEngine([
|
||||
PolicyRule(
|
||||
slug="allow-merge", project="ludarium", priority=10,
|
||||
description="allow merge", match={"tool": "gitea.pullrequest.merge"},
|
||||
decision=Decision.ALLOW,
|
||||
)
|
||||
])
|
||||
engine = PolicyEngine(
|
||||
[
|
||||
PolicyRule(
|
||||
slug="allow-merge",
|
||||
project="ludarium",
|
||||
priority=10,
|
||||
description="allow merge",
|
||||
match={"tool": "gitea.pullrequest.merge"},
|
||||
decision=Decision.ALLOW,
|
||||
)
|
||||
]
|
||||
)
|
||||
governance = Governance(engine, audit)
|
||||
evidence = {
|
||||
"repositoryRef": "test-repo", "pr": 7, "headSha": "e" * 40,
|
||||
"baseSha": "a" * 40, "requiredContext": "Managed validation / full (pull_request)",
|
||||
"validationRunId": 42, "validationEvent": "pull_request",
|
||||
"validationConclusion": "success", "mergeMethod": "squash",
|
||||
"repositoryRef": "test-repo",
|
||||
"pr": 7,
|
||||
"headSha": "e" * 40,
|
||||
"baseSha": "a" * 40,
|
||||
"requiredContext": "Managed validation / full (pull_request)",
|
||||
"validationRunId": 42,
|
||||
"validationEvent": "pull_request",
|
||||
"validationConclusion": "success",
|
||||
"mergeMethod": "squash",
|
||||
"resultingMainSha": "6" * 40,
|
||||
}
|
||||
result = governance.invoke(
|
||||
|
||||
@@ -93,6 +93,7 @@ def test_audit_read_denied_for_viewer_alone() -> None:
|
||||
"tool.invoke",
|
||||
"operations.check",
|
||||
"client.bootstrap",
|
||||
"client.verify",
|
||||
"client.disable",
|
||||
"client.revoke",
|
||||
],
|
||||
@@ -111,6 +112,7 @@ def test_client_lifecycle_actions_allowed_for_operator_and_admin(
|
||||
"tool.invoke",
|
||||
"operations.check",
|
||||
"client.bootstrap",
|
||||
"client.verify",
|
||||
"client.disable",
|
||||
"client.revoke",
|
||||
],
|
||||
|
||||
@@ -449,26 +449,31 @@ def test_only_reviewed_gitea_r3_lifecycle_set_is_allowed_in_production() -> None
|
||||
for tool in ("gitea.pullrequest.sync", "gitea.pullrequest.merge"):
|
||||
loop_trace = engine.evaluate(
|
||||
InvocationContext(
|
||||
workspace="itworx",
|
||||
project="gitea-loop",
|
||||
environment="prod",
|
||||
principal="chatgpt-gitea",
|
||||
tool=tool,
|
||||
connector="gitea",
|
||||
risk_class="R3",
|
||||
environment_classification="production",
|
||||
binding_present=True,
|
||||
client_enabled=True,
|
||||
workspace="itworx",
|
||||
project="gitea-loop",
|
||||
environment="prod",
|
||||
principal="chatgpt-gitea",
|
||||
tool=tool,
|
||||
connector="gitea",
|
||||
risk_class="R3",
|
||||
environment_classification="production",
|
||||
binding_present=True,
|
||||
client_enabled=True,
|
||||
)
|
||||
)
|
||||
assert loop_trace.decision == Decision.ALLOW_IF_CONDITIONS
|
||||
assert loop_trace.matched_rule_ids == ("036-gitea-loop-reviewed-lifecycle-v1",)
|
||||
denied = engine.evaluate(
|
||||
InvocationContext(
|
||||
workspace="itworx", project="gitea-loop", environment="prod",
|
||||
principal="chatgpt-gitea", tool="gitea.pullrequest.merge",
|
||||
connector="gitea", risk_class="R3",
|
||||
environment_classification="production", binding_present=False,
|
||||
workspace="itworx",
|
||||
project="gitea-loop",
|
||||
environment="prod",
|
||||
principal="chatgpt-gitea",
|
||||
tool="gitea.pullrequest.merge",
|
||||
connector="gitea",
|
||||
risk_class="R3",
|
||||
environment_classification="production",
|
||||
binding_present=False,
|
||||
client_enabled=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -204,8 +204,11 @@ def test_policy_simulate_available_to_every_role(
|
||||
def test_catalog_preview_available_to_every_role(
|
||||
role_sessions: dict[ManagementRole, RoleSession], role: ManagementRole
|
||||
) -> None:
|
||||
response = role_sessions[role].post("/api/catalog/preview", json={"project": "nonexistent"})
|
||||
response = role_sessions[role].post("/api/catalog/preview", json={"project": "ludarium"})
|
||||
assert response.status_code == 200, (role, response.text) # type: ignore[union-attr]
|
||||
preview = response.json() # type: ignore[union-attr]
|
||||
assert preview["valid"] is True
|
||||
assert preview["changes"] == ["reconcile all registered project routes, including ludarium"]
|
||||
|
||||
|
||||
# --- Tool readiness: bounded live probe, operator+ only ------------------
|
||||
@@ -400,6 +403,9 @@ def test_live_operations_check_available_to_operator_and_admin(
|
||||
response = role_sessions[role].post("/api/operations/check")
|
||||
assert response.status_code == 200, (role, response.text) # type: ignore[union-attr]
|
||||
assert response.json()["connectors"] # type: ignore[union-attr]
|
||||
assert {"id": "knowledge-default", "name": "Knowledge Default"} in response.json()[
|
||||
"connectors"
|
||||
]["itworx-knowledge"]["connectorRefs"] # type: ignore[union-attr]
|
||||
|
||||
|
||||
# --- Audit read: auditor+ only, viewer denied ---------------------------
|
||||
@@ -457,7 +463,7 @@ def test_client_create_allowed_for_admin(role_sessions: dict[ManagementRole, Rol
|
||||
assert response.status_code == 201, response.text # type: ignore[union-attr]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", ["bootstrap", "disable", "revoke"])
|
||||
@pytest.mark.parametrize("action", ["bootstrap", "verify", "disable", "revoke"])
|
||||
@pytest.mark.parametrize("role", [ManagementRole.VIEWER, ManagementRole.AUDITOR])
|
||||
def test_client_lifecycle_actions_denied_below_operator(
|
||||
role_sessions: dict[ManagementRole, RoleSession], role: ManagementRole, action: str
|
||||
@@ -470,7 +476,7 @@ def test_client_lifecycle_actions_denied_below_operator(
|
||||
assert response.status_code == 403, (role, action, response.text) # type: ignore[union-attr]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", ["bootstrap", "disable"])
|
||||
@pytest.mark.parametrize("action", ["bootstrap", "verify", "disable"])
|
||||
def test_client_lifecycle_actions_allowed_for_operator(
|
||||
role_sessions: dict[ManagementRole, RoleSession], action: str
|
||||
) -> None:
|
||||
|
||||
@@ -237,24 +237,20 @@ def test_dashboard_drift_is_measured_against_managed_connector_gateways(
|
||||
assert "connector gateway drift" in drift["detail"].lower()
|
||||
|
||||
|
||||
def test_dashboard_project_and_connector_health_matches_resource_list_not_hardcoded_unknown(
|
||||
def test_dashboard_distinguishes_registration_from_connector_readiness(
|
||||
viewer_client: TestClient,
|
||||
) -> None:
|
||||
"""The dashboard overview used to hardcode "unknown" for every project
|
||||
and connector regardless of catalog state, while the /api/resource
|
||||
list endpoint (GET /api/projects, /api/connectors) already computed a
|
||||
real "healthy"/"unknown" signal from catalog registration validity --
|
||||
the two surfaces visually disagreed. Both now share
|
||||
_catalog_registration_health(); this proves the dashboard reflects it
|
||||
too, not just the resource list.
|
||||
"""
|
||||
"""A valid registration is not evidence that a connector route is live."""
|
||||
dashboard = viewer_client.get("/api/dashboard")
|
||||
assert dashboard.status_code == 200
|
||||
body = dashboard.json()
|
||||
assert body["projects"], "expected at least one registered project in the test catalog"
|
||||
assert body["connectors"], "expected at least one registered connector in the test catalog"
|
||||
assert all(item["health"] == "healthy" for item in body["projects"])
|
||||
assert all(item["state"] == "healthy" for item in body["connectors"])
|
||||
assert all(item["state"] == "unknown" for item in body["connectors"])
|
||||
assert all(item["registrationState"] == "registered" for item in body["connectors"])
|
||||
assert all(item["readiness"]["source"] == "not-probed" for item in body["connectors"])
|
||||
assert all(item["readiness"]["observedAt"] is None for item in body["connectors"])
|
||||
|
||||
projects_list = viewer_client.get("/api/projects")
|
||||
assert projects_list.status_code == 200
|
||||
|
||||
Reference in new issue
Block a user