Files
ITWorx_MCP_Hub-Public/tests/test_compose.py
T

268 lines
12 KiB
Python

from __future__ import annotations
import json
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
COMPOSE = ROOT / "infrastructure" / "compose" / "docker-compose.yml"
class ComposeContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.document = yaml.safe_load(COMPOSE.read_text(encoding="utf-8"))
def test_all_images_are_immutable_or_local_dev(self) -> None:
for name, service in self.document["services"].items():
image = service.get("image", "")
self.assertNotIn(":latest", image, name)
if not image.startswith("local/"):
self.assertIn("@sha256:", image, name)
def test_socket_is_only_mounted_in_dedicated_broker(self) -> None:
for name, service in self.document["services"].items():
mounts = json.dumps(service.get("volumes", []))
if "/var/run/docker.sock" in mounts:
self.assertEqual(name, "docker-read-broker")
def test_only_edge_services_publish_ports(self) -> None:
for name, service in self.document["services"].items():
if service.get("ports"):
self.assertIn(name, {"web", "dataplane-router", "inspector"})
def test_broker_is_internal_and_only_socket_consumer(self) -> None:
broker = self.document["services"]["docker-read-broker"]
connector = self.document["services"]["connector-unraid"]
self.assertNotIn("ports", broker)
self.assertTrue(
any("/var/run/docker.sock" in str(value) for value in broker.get("volumes", []))
)
self.assertFalse(
any("/var/run/docker.sock" in str(value) for value in connector.get("volumes", []))
)
self.assertIn("docker_broker_service_token", broker.get("secrets", []))
self.assertIn("docker_broker_service_token", connector.get("secrets", []))
def test_wp050_integration_compose_is_isolated(self) -> None:
path = ROOT / "infrastructure/compose/docker-compose.wp050-integration.yml"
document = yaml.safe_load(path.read_text(encoding="utf-8"))
self.assertEqual(document["name"], "itworx-mcp-hub-wp050")
self.assertIn("@sha256:", document["services"]["contextforge"]["image"])
self.assertEqual(
document["services"]["contextforge"]["ports"],
["127.0.0.1:${WP050_CONTEXTFORGE_PORT:-18450}:4444"],
)
self.assertNotIn("ports", document["services"]["postgres"])
self.assertNotIn("ports", document["services"]["redis"])
def test_inspector_is_lan_only_optional_profile(self) -> None:
inspector = self.document["services"]["inspector"]
self.assertEqual(inspector.get("profiles"), ["inspector"])
self.assertIn(
"INSPECTOR_BIND_IP:?Set an explicit LAN-only Inspector bind address",
str(inspector["ports"]),
)
routing = (ROOT / "infrastructure/compose/NPM_CONNECTIVITY.md").read_text(encoding="utf-8")
self.assertIn("does not receive a public DNS route", routing)
def test_contextforge_and_hub_have_distinct_database_credentials(self) -> None:
init_script = (ROOT / "infrastructure/postgres/init/001-create-databases.sh").read_text(
encoding="utf-8"
)
self.assertIn("itworx_mcp_hub", init_script)
self.assertIn("contextforge", init_script)
self.assertIn("hub_database_password", init_script)
self.assertIn("contextforge_database_password", init_script)
def test_contextforge_admin_password_and_runtime_token_are_separate(self) -> None:
contextforge = self.document["services"]["contextforge"]
self.assertIn("contextforge_admin_credential", contextforge["secrets"])
self.assertNotIn("contextforge_api_token", contextforge["secrets"])
self.assertIn(
"contextforge-control",
contextforge["networks"]["hub-control"]["aliases"],
)
for name in ("control-api", "worker", "dataplane-router"):
service = self.document["services"][name]
self.assertIn("contextforge_api_token", service["secrets"])
self.assertEqual(service["environment"]["HUB_CONTEXTFORGE_AUTH_SCHEME"], "Bearer")
self.assertIn(
"contextforge-control:4444", service["environment"]["HUB_CONTEXTFORGE_URL"]
)
def test_dataplane_owns_mcp_admission_control(self) -> None:
contextforge = self.document["services"]["contextforge"]
router = self.document["services"]["dataplane-router"]
self.assertEqual(contextforge["environment"]["RATE_LIMITING_ENABLED"], "false")
self.assertEqual(
router["environment"]["HUB_RATE_LIMIT_PER_MINUTE"],
"${HUB_RATE_LIMIT_PER_MINUTE:-600}",
)
self.assertEqual(
router["environment"]["HUB_UPSTREAM_MAX_CONCURRENCY"],
"${HUB_UPSTREAM_MAX_CONCURRENCY:-24}",
)
self.assertEqual(
router["environment"]["HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS"],
"${HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS:-5}",
)
def test_public_oauth_contract_is_shared_by_web_and_dataplane(self) -> None:
web = self.document["services"]["web"]["environment"]
router = self.document["services"]["dataplane-router"]["environment"]
self.assertEqual(web["HUB_PUBLIC_MCP_URL"], router["HUB_PUBLIC_MCP_URL"])
for environment in (web, router):
self.assertIn(
"${HUB_MCP_ADDITIONAL_OIDC_ISSUER:-",
environment["HUB_MCP_ADDITIONAL_OIDC_ISSUER"],
)
def test_worker_health_requires_runtime_and_public_surface_readiness(self) -> None:
healthcheck = self.document["services"]["worker"]["healthcheck"]
self.assertIn("/health/ready", " ".join(healthcheck["test"]))
self.assertEqual(healthcheck["interval"], "10s")
self.assertEqual(healthcheck["retries"], 18)
web = self.document["services"]["web"]
self.assertEqual(web["environment"]["HUB_WORKER_URL"], "http://worker:8080")
self.assertEqual(web["depends_on"]["worker"]["condition"], "service_healthy")
def test_demo_token_revocations_require_an_operator_owned_host_file(self) -> None:
router = self.document["services"]["dataplane-router"]
self.assertEqual(router["user"], "${PUID:-10001}:${PGID:-10001}")
mount = next(value for value in router["volumes"] if ":/etc/itworx:ro" in value)
self.assertIn("DEMO_TOKEN_REVOCATIONS_DIR_HOST:?", mount)
self.assertNotIn("demo-token-revocations.example.json", mount)
def test_operator_tools_are_explicit_and_socket_free(self) -> None:
tools = self.document["services"]["operator-tools"]
self.assertEqual(tools["profiles"], ["operator-tools"])
self.assertEqual(tools["user"], "${PUID:-10001}:${PGID:-10001}")
self.assertEqual(tools["secrets"], ["demo_token_signing_key"])
self.assertNotIn("ports", tools)
self.assertNotIn("/var/run/docker.sock", json.dumps(tools["volumes"]))
self.assertIn("DEMO_TOKEN_REVOCATIONS_DIR_HOST:?", tools["volumes"][0])
self.assertIn(":/etc/itworx:rw", tools["volumes"][0])
prod = (ROOT / "infrastructure/compose/docker-compose.prod.yml").read_text(encoding="utf-8")
self.assertIn(
'image: "${HUB_DATAPLANE_IMAGE:?Set an immutable dataplane image digest}"', prod
)
self.assertIn(" operator-tools:\n", prod)
self.assertIn(" build: !reset null\n", prod)
def test_real_compose_config_has_no_build_for_owned_services(self) -> None:
docker = shutil.which("docker")
if docker is None:
self.skipTest("docker CLI is not available in this environment")
prod_compose = ROOT / "infrastructure" / "compose" / "docker-compose.prod.yml"
env_file = ROOT / "tests" / "fixtures" / "wp200-compose-config.env"
required_secrets = (
"postgres_superuser_password",
"hub_database_password",
"contextforge_database_password",
"contextforge_auth_encryption_secret",
"contextforge_jwt_secret",
"contextforge_admin_credential",
"contextforge_api_token",
"docker_broker_service_token",
"connector_identity_signing_key",
"demo_token_signing_key",
"authentik_oidc_client_secret",
"hub_session_signing_key",
"gitea_mcp_token",
"gitea_mcp_write_token",
"stitch_api_key",
"redis_password",
)
with tempfile.TemporaryDirectory() as secrets_dir:
for name in required_secrets:
(Path(secrets_dir) / name).write_text("wp200-fixture-secret\n", encoding="utf-8")
env = {**os.environ, "SECRETS_DIR": secrets_dir}
completed = subprocess.run( # noqa: S603
[
docker,
"compose",
"-f",
str(COMPOSE),
"-f",
str(prod_compose),
"--env-file",
str(env_file),
"config",
"--format",
"json",
],
capture_output=True,
text=True,
check=False,
env=env,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
rendered = json.loads(completed.stdout)
# Derived from the base compose file's own `build:` keys (not a hand-maintained
# duplicate list) so this test automatically covers any future service that
# gains a `build:` section, rather than silently skipping it. See
# evidence/WP-200/ADVERSARIAL_REVIEW.md item (c).
owned_with_build = tuple(
name
for name, service in self.document["services"].items()
if "build" in service and not service.get("profiles")
)
self.assertTrue(owned_with_build, "no service in the base compose file defines build:")
for name in owned_with_build:
self.assertIn(
name, rendered["services"], f"{name} is missing from the rendered topology"
)
self.assertNotIn(
"build",
rendered["services"][name],
f"{name} retains a build definition; local build fallback is possible",
)
def test_unraid_folder_view_groups_the_complete_production_stack(self) -> None:
definition_path = ROOT / "infrastructure" / "unraid" / "folder-view3-itworx-mcp-hub.json"
definition = json.loads(definition_path.read_text(encoding="utf-8"))
folder = definition["ITWorxMCPHub"]
expected_containers = {
"itworx-mcp-hub-postgres-1",
"itworx-mcp-hub-redis-1",
"itworx-mcp-hub-contextforge-1",
"itworx-mcp-hub-control-api-1",
"itworx-mcp-hub-worker-1",
"itworx-mcp-hub-dataplane-router-1",
"itworx-mcp-hub-web-1",
"itworx-mcp-hub-connector-knowledge-1",
"itworx-mcp-hub-connector-gitea-1",
"itworx-mcp-hub-connector-unraid-1",
"itworx-mcp-hub-connector-mobilityops-1",
"itworx-mcp-hub-connector-projectbrain-1",
"itworx-mcp-hub-connector-appops-1",
"itworx-mcp-hub-connector-stitch-1",
"itworx-mcp-hub-docker-read-broker-1",
}
self.assertEqual(folder["name"], "ITWorx MCP Hub")
self.assertEqual(folder["regex"], "^itworx-mcp-hub-")
self.assertEqual(set(folder["containers"]), expected_containers)
self.assertTrue(folder["settings"]["folder_webui"])
self.assertEqual(folder["settings"]["folder_webui_url"], "http://198.18.10.150:1100")
self.assertEqual(folder["icon"], "https://mcp.example.com/itworx-mcp-hub-icon.png")
icon_path = ROOT / "apps" / "web" / "public" / "itworx-mcp-hub-icon.png"
self.assertTrue(icon_path.is_file())
self.assertEqual(icon_path.read_bytes()[:8], b"\x89PNG\r\n\x1a\n")
self.assertLess(icon_path.stat().st_size, 2 * 1024 * 1024)
if __name__ == "__main__":
unittest.main()