from __future__ import annotations import json import os import sys import unittest import uuid from copy import deepcopy from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) class ExternalCredentialCryptoTests(unittest.TestCase): def test_requires_master_key_and_never_falls_back_to_plaintext(self): from core.external_systems.crypto import encrypt_secret with patch.dict(os.environ, {}, clear=True): with self.assertRaisesRegex(RuntimeError, "ZCBOT_CREDENTIAL_MASTER_KEY"): encrypt_secret("secret") def test_roundtrip_uses_ciphertext(self): from core.external_systems.crypto import decrypt_secret, encrypt_secret with patch.dict(os.environ, {"ZCBOT_CREDENTIAL_MASTER_KEY": "unit-test-key-at-least-32-characters"}, clear=False): stored = encrypt_secret("mes-password") self.assertTrue(stored.startswith("v1:")) self.assertNotIn("mes-password", stored) self.assertEqual(decrypt_secret(stored), "mes-password") def test_rejects_short_master_key(self): from core.external_systems.crypto import configured, encrypt_secret with patch.dict(os.environ, {"ZCBOT_CREDENTIAL_MASTER_KEY": "too-short"}, clear=False): self.assertFalse(configured()) with self.assertRaisesRegex(RuntimeError, "至少需要 32"): encrypt_secret("mes-password") def _cfg(*, allowed=frozenset()): from core.external_systems.factory import FactoryMesConfig return FactoryMesConfig( base_url="https://factory.invalid", openapi_url="https://factory.invalid/swagger.json", login_path="/api/auth/token/", allowed_post_operations=frozenset(allowed), timeout_seconds=5, max_result_bytes=65536, verify_tls=True, ) _SPEC = { "swagger": "2.0", "paths": { "/api/qm/ftestwork/{batch}/": { "get": { "operationId": "qm_ftestwork_read", "summary": "查询成品检验批次", "tags": ["quality"], "parameters": [ {"name": "batch", "in": "path", "required": True, "type": "string"}, {"name": "page_size", "in": "query", "required": False, "type": "integer"}, ], } }, "/api/bi/dataset/{code}/exec/": { "post": { "operationId": "bi_dataset_exec", "summary": "执行只读数据集", "parameters": [ {"name": "code", "in": "path", "required": True, "type": "string"}, {"name": "payload", "in": "body", "required": True, "schema": {"type": "object"}}, ], } }, }, } class _Response: def __init__(self, status_code=200, payload=None): self.status_code = status_code self._payload = payload self.headers = {"content-type": "application/json"} self.text = json.dumps(payload, ensure_ascii=False) def json(self): return self._payload class _Http: def __init__(self): self.calls = [] def __enter__(self): return self def __exit__(self, *args): return False def post(self, url, **kwargs): self.calls.append(("POST", url, kwargs)) return _Response(payload={"access": "remote-jwt"}) def get(self, url, **kwargs): self.calls.append(("GET", url, kwargs)) return _Response(payload=_SPEC) def request(self, method, url, **kwargs): self.calls.append((method, url, kwargs)) return _Response(payload={"count": 1, "results": [{"batch": "B/1"}]}) class FactoryOpenApiConnectorTests(unittest.TestCase): def setUp(self): from core.external_systems import factory factory._SPEC_CACHE.clear() def test_admin_mapping_builds_bounded_runtime_config(self): from core.external_systems.factory import FactoryMesConfig cfg = FactoryMesConfig.from_mapping({ "base_url": "https://factory.invalid/", "openapi_url": "https://factory.invalid/swagger.json", "allowed_post_operations": "bi_dataset_exec, report_preview", "timeout_seconds": 999, "max_result_bytes": 1, "verify_tls": True, }) self.assertEqual(cfg.base_url, "https://factory.invalid") self.assertEqual(cfg.timeout_seconds, 60) self.assertEqual(cfg.max_result_bytes, 4096) self.assertEqual(cfg.allowed_post_operations, {"bi_dataset_exec", "report_preview"}) def test_admin_mapping_rejects_embedded_url_credentials(self): from core.external_systems.factory import FactoryMesConfig, FactoryMesError with self.assertRaisesRegex(FactoryMesError, "不能内嵌凭据"): FactoryMesConfig.from_mapping({ "base_url": "https://user:secret@factory.invalid", "openapi_url": "https://factory.invalid/swagger.json", }) def test_search_discovers_operation_without_exposing_credentials(self): from core.external_systems.factory import FactoryMesClient http = _Http() client = FactoryMesClient("mes-user", "mes-password", _cfg()) with patch.object(client, "_client", return_value=http): result = client.search("成品检验") self.assertEqual(result[0]["operation_id"], "qm_ftestwork_read") rendered = json.dumps(result, ensure_ascii=False) self.assertNotIn("mes-password", rendered) self.assertNotIn("remote-jwt", rendered) def test_get_call_resolves_encoded_path_and_query(self): from core.external_systems.factory import FactoryMesClient http = _Http() client = FactoryMesClient("mes-user", "mes-password", _cfg()) with patch.object(client, "_client", return_value=http): result = client.call( "qm_ftestwork_read", arguments={"batch": "B/1", "page_size": 50}, ) method, url, kwargs = [call for call in http.calls if call[0] == "GET" and "/api/" in call[1]][0] self.assertEqual(method, "GET") self.assertIn("B%2F1", url) self.assertEqual(kwargs["params"], {"page_size": 50}) self.assertEqual(result["data"]["count"], 1) def test_swagger_base_path_is_added_to_operation_url(self): from core.external_systems.factory import FactoryMesClient spec = deepcopy(_SPEC) spec["basePath"] = "/api" spec["paths"] = { path.removeprefix("/api"): value for path, value in spec["paths"].items() } http = _Http() client = FactoryMesClient("u", "p", _cfg()) with patch.object(client, "_client", return_value=http), patch.object( client, "_fetch_spec", return_value=spec ): client.call("qm_ftestwork_read", arguments={"batch": "B1"}) request = next(call for call in http.calls if call[0] == "GET") self.assertEqual(request[1], "https://factory.invalid/api/qm/ftestwork/B1/") def test_api_base_path_does_not_change_login_url(self): from core.external_systems.factory import FactoryMesClient http = _Http() client = FactoryMesClient("u", "p", _cfg()) with patch.object(client, "_client", return_value=http): client.authenticate() request = next(call for call in http.calls if call[0] == "POST") self.assertEqual(request[1], "https://factory.invalid/api/auth/token/") def test_base_path_is_not_duplicated_when_operation_already_contains_it(self): from core.external_systems.factory import FactoryMesClient, FactoryMesConfig spec = {**deepcopy(_SPEC), "basePath": "/api"} http = _Http() client = FactoryMesClient("u", "p", _cfg()) with patch.object(client, "_client", return_value=http), patch.object( client, "_fetch_spec", return_value=spec ): client.call("qm_ftestwork_read", arguments={"batch": "B1"}) request = next(call for call in http.calls if call[0] == "GET") self.assertNotIn("/api/api/", request[1]) configured_prefix = FactoryMesClient( "u", "p", FactoryMesConfig( **{**_cfg().__dict__, "base_url": "https://factory.invalid/api"} ), ) self.assertEqual( configured_prefix._operation_url(spec, "/api/qm/ftestwork/B1/"), "https://factory.invalid/api/qm/ftestwork/B1/", ) def test_openapi_server_path_is_used_but_cross_origin_server_is_rejected(self): from core.external_systems.factory import FactoryMesClient, FactoryMesError client = FactoryMesClient("u", "p", _cfg()) same_origin = {"openapi": "3.0.0", "servers": [{"url": "/v1"}], "paths": {}} self.assertEqual( client._operation_url(same_origin, "/quality/results/"), "https://factory.invalid/v1/quality/results/", ) cross_origin = { "openapi": "3.0.0", "servers": [{"url": "https://attacker.invalid/v1"}], "paths": {}, } with self.assertRaisesRegex(FactoryMesError, "越出 Factory MES 主机"): client._operation_url(cross_origin, "/quality/results/") def test_no_declared_base_path_keeps_existing_url_behavior(self): from core.external_systems.factory import FactoryMesClient client = FactoryMesClient("u", "p", _cfg()) self.assertEqual( client._operation_url(_SPEC, "/api/qm/ftestwork/B1/"), "https://factory.invalid/api/qm/ftestwork/B1/", ) def test_post_is_denied_unless_admin_allowlists_operation(self): from core.external_systems.factory import FactoryMesClient, FactoryMesError denied = FactoryMesClient("u", "p", _cfg()) with patch.object(denied, "authenticate", return_value="jwt"), patch.object( denied, "_fetch_spec", return_value=_SPEC ): with self.assertRaisesRegex(FactoryMesError, "只读调用范围"): denied.call("bi_dataset_exec", arguments={"code": "x", "payload": {}}) http = _Http() allowed = FactoryMesClient("u", "p", _cfg(allowed={"bi_dataset_exec"})) with patch.object(allowed, "_client", return_value=http): result = allowed.call( "bi_dataset_exec", arguments={"code": "yield", "payload": {"query": {"month": "2026-08"}}}, ) request = [call for call in http.calls if call[0] == "POST" and "/dataset/" in call[1]][0] self.assertEqual(request[2]["json"], {"query": {"month": "2026-08"}}) self.assertFalse(result["truncated"]) def test_allowlisted_post_accepts_separate_body_field(self): from core.external_systems.factory import FactoryMesClient http = _Http() allowed = FactoryMesClient("u", "p", _cfg(allowed={"bi_dataset_exec"})) with patch.object(allowed, "_client", return_value=http): result = allowed.call( "bi_dataset_exec", arguments={"code": "quality"}, body={"batch": "B-1"}, ) request = [call for call in http.calls if call[0] == "POST" and "/dataset/" in call[1]][0] self.assertEqual(request[2]["json"], {"batch": "B-1"}) self.assertFalse(result["truncated"]) def test_rejects_unknown_arguments(self): from core.external_systems.factory import FactoryMesClient, FactoryMesError client = FactoryMesClient("u", "p", _cfg()) with patch.object(client, "authenticate", return_value="jwt"), patch.object( client, "_fetch_spec", return_value=_SPEC ): with self.assertRaisesRegex(FactoryMesError, "接口定义之外"): client.call( "qm_ftestwork_read", arguments={"batch": "B1", "unexpected": "x"}, ) class ExternalSystemToolSafetyTests(unittest.TestCase): def test_tools_are_scoped_to_constructor_user(self): from tools.external_systems import ExternalSystemListTool uid = uuid.uuid4() with patch( "tools.external_systems.list_external_systems", return_value=[{ "external_system_id": str(uuid.uuid4()), "status": "active", "username_masked": "me***r", }], ) as listed: output = ExternalSystemListTool(uid).execute() listed.assert_called_once_with(uid) self.assertNotIn("password", output.lower()) if __name__ == "__main__": unittest.main()