Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:36:21 +02:00
commit cb5be898ea
663 files changed
+82674

No files matched your search

+261
View File
@@ -0,0 +1,261 @@
"""WP-230C regression tests: pagination, project detail, dashboard honesty.
Reuses the same real-app-with-fakes construction pattern as
tests/test_wp230_rbac.py (real PKCE/BFF login against a fake OIDC provider,
a real build_app() instance, a throwaway SQLite engine) rather than
importing that file's fixtures directly, since pytest only auto-discovers
fixtures within the same file (or conftest.py, deliberately not added here
to avoid affecting unrelated tests).
"""
from __future__ import annotations
import base64
import os
from collections.abc import Iterator
from datetime import UTC, datetime, timedelta
from http.cookies import SimpleCookie
from pathlib import Path
from urllib.parse import parse_qs, urlparse
import pytest
from fastapi import HTTPException
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 ManagementRole
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.runtime.control_api import build_app, decode_page_cursor, encode_page_cursor
from services.runtime.state_sync import sync_runtime_state
ROOT = Path(__file__).resolve().parents[1]
ISSUER = "https://fake.oidc.test"
@pytest.fixture(scope="module", autouse=True)
def _client_bindings_env() -> Iterator[None]:
previous = os.environ.get("MCP_CLIENT_BINDINGS_JSON")
os.environ["MCP_CLIENT_BINDINGS_JSON"] = "[]"
yield
if previous is None:
os.environ.pop("MCP_CLIENT_BINDINGS_JSON", None)
else:
os.environ["MCP_CLIENT_BINDINGS_JSON"] = previous
@pytest.fixture(scope="module")
def db_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
path = tmp_path_factory.mktemp("wp230-ui") / "ui.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_response = client.get("/auth/login", follow_redirects=False)
location = login_response.headers["location"]
query = parse_qs(urlparse(location).query)
code = provider.authorize(location)
callback_response = client.get(
"/auth/callback",
params={"code": code, "state": query["state"][0]},
follow_redirects=False,
)
cookie = SimpleCookie()
cookie.load(callback_response.headers["set-cookie"])
cookie_name = next(iter(cookie))
client.cookies.set(cookie_name, cookie[cookie_name].value)
session_response = client.get("/auth/session")
return str(session_response.json()["csrf_token"])
@pytest.fixture(scope="module")
def viewer_client(db_path: Path) -> Iterator[TestClient]:
provider = FakeOIDCProvider(issuer=ISSUER, user_groups=("hub-viewer",))
oidc = OIDCSettings(
issuer=ISSUER,
audience=provider.audience,
client_id="hub-bff",
redirect_uri="https://hub.test/auth/callback",
secure_cookie=True,
role_group_map={"hub-viewer": ManagementRole.VIEWER},
)
contextforge_fake = FakeContextForge()
os.environ["WP230UI_FAKE_CONTEXTFORGE_TOKEN"] = "fake-token" # noqa: S105
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="WP230UI_FAKE_CONTEXTFORGE_TOKEN", # noqa: S106
),
contextforge_transport=contextforge_fake.transport(),
engine=engine,
catalog_root=ROOT / "catalog",
)
client = TestClient(app)
_login(client, provider)
yield client
client.close()
# --- Cursor encode/decode: pure functions -------------------------------
def test_cursor_round_trips() -> None:
cursor = encode_page_cursor(25)
assert decode_page_cursor(cursor) == 25
def test_malformed_cursor_is_rejected_with_400() -> None:
with pytest.raises(HTTPException) as excinfo:
decode_page_cursor("not-a-real-cursor!!")
assert excinfo.value.status_code == 400
def test_negative_offset_cursor_is_rejected() -> None:
forged = base64.urlsafe_b64encode(b"-5:9999999999").decode("ascii")
with pytest.raises(HTTPException) as excinfo:
decode_page_cursor(forged)
assert excinfo.value.status_code == 400
def test_expired_cursor_is_rejected_not_silently_reset() -> None:
long_ago = int((datetime.now(UTC) - timedelta(hours=2)).timestamp())
forged = base64.urlsafe_b64encode(f"10:{long_ago}".encode()).decode("ascii")
with pytest.raises(HTTPException) as excinfo:
decode_page_cursor(forged)
assert excinfo.value.status_code == 400
assert "expired" in str(excinfo.value.detail)
# --- Pagination through a real endpoint ----------------------------------
def test_first_page_has_a_next_cursor_when_more_rows_exist(viewer_client: TestClient) -> None:
response = viewer_client.get("/api/tools?limit=5")
assert response.status_code == 200
body = response.json()
assert len(body["rows"]) == 5
assert body["page"]["total"] > 5
assert body["page"]["nextCursor"] is not None
def test_following_next_cursor_returns_a_disjoint_next_page(viewer_client: TestClient) -> None:
first = viewer_client.get("/api/tools?limit=5").json()
second = viewer_client.get(f"/api/tools?limit=5&cursor={first['page']['nextCursor']}").json()
first_ids = {row["id"] for row in first["rows"]}
second_ids = {row["id"] for row in second["rows"]}
assert first_ids.isdisjoint(second_ids)
def test_last_page_has_no_next_cursor(viewer_client: TestClient) -> None:
page = viewer_client.get("/api/tools?limit=100").json()
total = page["page"]["total"]
row_count = len(page["rows"])
while page["page"]["nextCursor"] is not None:
page = viewer_client.get(f"/api/tools?limit=100&cursor={page['page']['nextCursor']}").json()
row_count += len(page["rows"])
assert page["page"]["nextCursor"] is None
assert row_count == total
def test_invalid_cursor_query_param_fails_safely_with_400(viewer_client: TestClient) -> None:
response = viewer_client.get("/api/tools?cursor=not-valid-base64!!")
assert response.status_code == 400
def test_page_total_reflects_full_match_count_not_just_this_page(
viewer_client: TestClient,
) -> None:
# This was a real, distinct bug: the old code computed "total" *after*
# truncating rows to the page size, so it was always equal to the page
# length, not the true total -- "N total" in the UI was never accurate
# beyond a single page.
full = viewer_client.get("/api/tools?limit=100").json()
small_page = viewer_client.get("/api/tools?limit=1").json()
assert small_page["page"]["total"] == full["page"]["total"]
assert len(small_page["rows"]) == 1
# --- Real project detail --------------------------------------------------
def test_project_detail_returns_real_registered_environments(viewer_client: TestClient) -> None:
response = viewer_client.get("/api/projects/ludarium")
assert response.status_code == 200
body = response.json()
assert body["slug"] == "ludarium"
environment_names = {item["name"] for item in body["environments"]}
assert "prod" in environment_names
assert "staging" not in environment_names # ludarium never registered one
def test_project_detail_unknown_slug_is_404_not_a_fabricated_default(
viewer_client: TestClient,
) -> None:
response = viewer_client.get("/api/projects/does-not-exist")
assert response.status_code == 404
def test_project_detail_reflects_real_connector_bindings(viewer_client: TestClient) -> None:
body = viewer_client.get("/api/projects/ludarium").json()
connector_types = {item["connectorType"] for item in body["connectorBindings"]}
assert "knowledge" in connector_types
# --- Dashboard drift honesty ------------------------------------------------
def test_dashboard_drift_is_measured_against_managed_connector_gateways(
viewer_client: TestClient,
) -> None:
response = viewer_client.get("/api/dashboard")
assert response.status_code == 200
drift = response.json()["drift"]
assert drift["known"] is True
assert drift["count"] == 9
assert drift["projects"] == [
"itworx-appops",
"itworx-gitea",
"itworx-knowledge",
"itworx-mobilityops",
"itworx-opencode",
"itworx-opencode-readonly",
"itworx-projectbrain",
"itworx-stitch",
"itworx-unraid",
]
assert "connector gateway drift" in drift["detail"].lower()
def test_dashboard_project_and_connector_health_matches_resource_list_not_hardcoded_unknown(
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.
"""
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"])
projects_list = viewer_client.get("/api/projects")
assert projects_list.status_code == 200
assert all(row["status"] == "healthy" for row in projects_list.json()["rows"])