Files

69 lines
4.3 KiB
Python

import hashlib
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from console.settings import Settings
from console.store import Store
from console.native_plans import build_plan
from console.native_rpc import handle
class NativeRPCTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name)
source = root / "source"
source.mkdir()
entry = source / "test.py"
entry.write_text("print(1)\n")
self.settings = Settings(root / "runtime", [{"id": "original", "path": str(source)}])
self.settings.test_definitions = [{"key": "native", "source_group": "original", "entry": "test.py", "name": "原环境样例"}]
self.settings.native_catalog = {"targets": [], "profiles": [{"id": "native", "source_group": "original", "entry": "test.py",
"cwd": str(source), "user": "root", "interpreter": "/usr/bin/python3", "parameters": [], "arguments": [],
"impact": {"level": "service", "title": "服务切换", "details": ["会停止指定服务"], "requires_confirmation": True}}]}
self.script = {"id": "a" * 32, "name": "test.py", "description": "", "kind": "external", "source_group": "original",
"source_path": str(entry), "sha256": hashlib.sha256(entry.read_bytes()).hexdigest(), "language": "python", "version": 1,
"enabled": True, "status": "ready", "config": {"native_options": {}, "timeout_seconds": 300}}
self.store = Store(self.settings)
self.store.save_script(self.script)
self.plan = build_plan(self.settings, self.script, check_host=False)
self.request = {"op": "queue", "script_id": self.script["id"], "request_id": "5b0202b7-851d-48ad-a7b9-dce22f000001",
"plan_token": self.plan["plan_token"], "source_ip": "127.0.0.1"}
def tearDown(self):
self.temp.cleanup()
def test_server_requires_explicit_current_confirmation(self):
with patch("console.native_rpc.native_plans.build_plan", return_value=self.plan):
with self.assertRaises(ValueError):handle(self.settings, self.store, self.request)
with self.assertRaises(ValueError):handle(self.settings, self.store, {**self.request, "impact_acknowledged": True, "plan_token": "old"})
self.assertEqual(self.store.runs(), [])
run = handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
self.assertTrue(run["execution"]["impact_acknowledged"])
self.assertEqual(run["execution"]["mode"], "native")
def test_retry_returns_original_even_if_plan_later_changed(self):
with patch("console.native_rpc.native_plans.build_plan", return_value=self.plan):
first = handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
with patch("console.native_rpc.native_plans.build_plan", side_effect=ValueError("changed")):
again = handle(self.settings, self.store, self.request)
self.assertEqual(first["id"], again["id"])
self.assertEqual(len(self.store.runs()), 1)
def test_failed_preflight_and_unknown_operation_never_queue(self):
with patch("console.native_rpc.native_plans.build_plan", return_value={**self.plan, "ready": False}):
with self.assertRaises(ValueError):handle(self.settings, self.store, {**self.request, "impact_acknowledged": True})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "exec", "command": "id"})
self.assertEqual(self.store.runs(), [])
def test_input_rejected_for_queued_or_noninteractive_run(self):
run = self.store.queue(self.script, "sample", "127.0.0.1", execution={"mode": "native", "interactive": True})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "input", "run_id": run["id"], "text": "1"})
with self.assertRaises(ValueError):handle(self.settings, self.store, {"op": "input", "run_id": "../../bad", "text": "1"})
def test_input_channel_rejects_tmux_keys_and_shell_commands(self):
for text in ("\x02c", "\x1b", "bash", "1\nid", "echo test", ":new-window"):
with self.assertRaisesRegex(ValueError, "控制键"):
handle(self.settings, self.store, {"op": "input", "run_id": "a"*32, "text": text})