Ctrl K
ComunidadGuías…

tests/test_server.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""无桌面入口的进程退出与现有数据保护。"""
import subprocess
import sys
import tempfile
import threading
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

from peach import server, settings_file
from starlette.requests import Request


class ServerTests(unittest.TestCase):
    def test_reader_initialization_does_not_launch_a_ledger_migration(self):
        with tempfile.TemporaryDirectory() as directory:
            config = settings_file.load_config(environ={'PEACH_DATA_ROOT': str(Path(directory).resolve())})
            settings_file.write(config)
            config.directory('database').mkdir()
            database = config.directory('database') / 'ledger.db'
            database.write_bytes(b'unchanged reader ledger')
            config = settings_file.load_config(environ={'PEACH_DATA_ROOT': str(config.data_root)})
            with patch.object(server.settings_file, 'load_config', return_value=config), patch(
                    'peach.runtime_prepare.configured_ledger_writer', return_value=(False, '读取端')), \
                    patch.object(server, 'launch') as launch:
                server.initialize()
            launch.assert_not_called()
            self.assertEqual(database.read_bytes(), b'unchanged reader ledger')

    def test_headless_configuration_accepts_private_peers_and_rejects_forwarding(self):
        from peach.routes_configuration import local_client, managed_configuration
        with patch.dict("os.environ", {"PEACH_HEADLESS_MANAGED": "1"}):
            self.assertTrue(managed_configuration())
            for peer, headers, allowed in (
                ("192.0.2.1", [], True), ("198.51.100.10", [], True),
                ("8.8.8.8", [], False), ("169.254.1.1", [], False),
                ("0.0.0.0", [], False), ("240.0.0.1", [], False),
                ("127.0.0.1", [], True), ("fd00::1", [], True),
                ("192.0.2.1", [(b"x-forwarded-for", b"8.8.8.8")], False),
                ("192.0.2.1", [(b"x-forwarded-host", b"example.com")], False),
            ):
                request = Request({"type": "http", "client": (peer, 1234),
                                   "server": ("0.0.0.0", 8900), "headers": headers,
                                   "app": SimpleNamespace(state=SimpleNamespace(tunnel=None))})
                with self.subTest(peer=peer, headers=headers):
                    self.assertEqual(local_client(request), allowed)

    def test_cli_without_mdns_or_tunnel_needs_no_network_route(self):
        from peach import cli
        with patch.object(cli, "_require_readable_settings"), \
                patch.object(cli, "_serve_token", return_value="test"), \
                patch.object(cli, "_build_sync", return_value=None), \
                patch.object(cli, "create_app"), patch.object(cli, "TUNNEL_ENABLED", False), \
                patch("peach.mdns.lan_ipv4", side_effect=OSError("no route")) as route, \
                patch.object(cli, "_run_uvicorn"):
            self.assertEqual(cli.main(["serve", "--host", "0.0.0.0", "--no-mdns"]), 0)
        route.assert_not_called()

    def test_every_served_listener_gets_the_handler_that_can_listen_again(self):
        """客户端在 accept 完成前断开、监听被标准库关掉时,处理器要拿着 uvicorn 的 Server 才能重新监听。"""
        import asyncio
        from peach import cli, error_reporting
        handlers = []
        for arguments in (["serve", "--host", "0.0.0.0", "--no-mdns"],
                          ["serve", "--host", "0.0.0.0", "--port", "80", "--redirect-origin", "https://peach.test"]):
            handlers.clear()
            with self.subTest(arguments=arguments), \
                    patch.object(cli, "_require_readable_settings"), \
                    patch.object(cli, "_serve_token", return_value="test"), \
                    patch.object(cli, "_build_sync", return_value=None), \
                    patch.object(cli, "create_app"), patch.object(cli, "TUNNEL_ENABLED", False), \
                    patch("uvicorn.Config") as config, patch("uvicorn.Server") as uvicorn_server:
                config.return_value.get_loop_factory.return_value = None
                uvicorn_server.return_value.serve = AsyncMock(
                    side_effect=lambda: handlers.append(asyncio.get_running_loop().get_exception_handler()))
                self.assertEqual(cli.main(arguments), 0)
                handler, = handlers
                self.assertIs(handler.func, error_reporting.asyncio_handler)
                self.assertEqual(handler.keywords, {"uvicorn_server": uvicorn_server.return_value})

    def test_initialization_preserves_existing_configuration(self):
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory).resolve()
            config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(root)})
            settings_file.write(config)
            config.directory('database').mkdir()
            config.directory('database').joinpath('ledger.db').touch()
            existing = config.path.read_bytes()
            with patch.object(server.settings_file, "load_config", return_value=settings_file.load_config(
                    environ={"PEACH_DATA_ROOT": str(root)})), patch.object(server, "launch") as launch:
                launch.return_value.wait.return_value = 0
                server.initialize()
            launch.assert_called_once_with('migrate', 'upgrade', '--yes', '--db', str(config.directory('database') / 'ledger.db'))
            self.assertEqual(config.path.read_bytes(), existing)

    def test_initialization_rejects_database_without_configuration(self):
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory).resolve()
            config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(root)})
            config.directory("database").mkdir()
            config.directory("database").joinpath("ledger.db").write_bytes(b"existing")
            with patch.object(server.settings_file, "load_config", return_value=config), \
                    patch.object(server, "launch") as launch:
                with self.assertRaisesRegex(RuntimeError, "已有数据库"):
                    server.initialize()
            launch.assert_not_called()

    def test_stop_waits_for_child_exit(self):
        process = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"],
                                   stdin=subprocess.DEVNULL)
        try:
            server.stop_process(process)
            self.assertIsNotNone(process.poll())
        finally:
            server.stop_process(process)

    def test_relocation_restarts_the_service_on_the_new_data_root(self):
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory).resolve()
            config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(root)})
            process = subprocess.Popen([sys.executable, "-c", "raise SystemExit(17)"], stdin=subprocess.DEVNULL)
            process.wait(timeout=15)
            calls = []

            def relocation(stop, restart):
                if calls:
                    return False
                calls.append(True)
                stop()
                restart({"PEACH_DATA_ROOT": str(root / "moved")})
                return True

            with patch.dict(server.os.environ, {}), \
                    patch.object(server.settings_file, "load_config", return_value=config), \
                    patch("peach.data_storage.poll_headless", side_effect=relocation), \
                    patch.object(server, "launch", return_value=process) as launch:
                self.assertEqual(server.supervise("127.0.0.1", 8900, threading.Event()), 17)
                # 迁移后看守进程自己也按新位置读设置,拉起的服务继承这个位置。
                self.assertEqual(server.os.environ["PEACH_DATA_ROOT"], str(root / "moved"))
            self.assertEqual(launch.call_count, 1)

    def test_service_failure_exits_supervisor(self):
        with tempfile.TemporaryDirectory() as directory:
            config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(Path(directory).resolve())})
            process = subprocess.Popen([sys.executable, "-c", "raise SystemExit(17)"],
                                       stdin=subprocess.DEVNULL)
            process.wait(timeout=15)
            with patch.object(server.settings_file, "load_config", return_value=config), \
                    patch.object(server, "launch", return_value=process):
                self.assertEqual(server.supervise("127.0.0.1", 8900, threading.Event()), 17)

    def test_a_fresh_docker_volume_starts_unconfigured_without_init(self):
        with tempfile.TemporaryDirectory() as directory:
            config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(Path(directory).resolve())})
            with patch.dict(server.os.environ, {"PEACH_DISTRIBUTION": "docker"}), \
                    patch.object(server.settings_file, "load_config", return_value=config), \
                    patch.object(server, "launch") as launch:
                server.initialize()
                launch.assert_not_called()
                self.assertFalse(config.path.exists())
                config.directory("database").mkdir(parents=True)
                config.directory("database").joinpath("ledger.db").write_bytes(b"existing")
                with self.assertRaisesRegex(RuntimeError, "已有数据库"):
                    server.initialize()
            launch.assert_not_called()

    def test_the_setup_service_is_replaced_once_the_settings_file_appears(self):
        from dataclasses import replace
        from unittest.mock import Mock
        with tempfile.TemporaryDirectory() as directory:
            absent = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(Path(directory).resolve())})
            present = replace(absent, present=True)
            state = {"configured": False}
            stopping = threading.Event()
            launched = []

            def start(*arguments):
                launched.append(arguments)
                if len(launched) == 1:
                    state["configured"] = True  # 首次设置提交,设置文件写好了
                else:
                    stopping.set()
                return Mock(poll=Mock(return_value=None))

            with patch.object(server.settings_file, "load_config",
                              side_effect=lambda: present if state["configured"] else absent), \
                    patch("peach.data_storage.poll_headless", return_value=False), \
                    patch.object(server, "launch", side_effect=start):
                self.assertEqual(server.supervise("0.0.0.0", 8900, stopping), 0)
        self.assertEqual([arguments[:2] for arguments in launched], [("serve", "--setup"), ("serve", "--host")])

    def test_access_password_from_the_environment_is_written_once_and_never_echoed(self):
        from peach import access
        with tempfile.TemporaryDirectory() as directory:
            config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(Path(directory).resolve())})
            path = config.directory("secrets") / "access.json"
            with patch.dict(server.os.environ, {server.ACCESS_PASSWORD_ENV: "short"}):
                message = server.seed_access_password(config)
                self.assertNotIn(server.ACCESS_PASSWORD_ENV, server.os.environ)
            self.assertIn("8–256", message)
            self.assertFalse(path.exists())
            with patch.dict(server.os.environ, {server.ACCESS_PASSWORD_ENV: "container-secret"}):
                message = server.seed_access_password(config)
                self.assertNotIn(server.ACCESS_PASSWORD_ENV, server.os.environ, "不传给服务子进程")
            self.assertNotIn("container-secret", message)
            policy = access.load(path)
            self.assertEqual(policy["mode"], "password")
            self.assertTrue(access.verify(policy, "container-secret"))
            self.assertNotIn("container-secret", path.read_text(encoding="utf-8"))
            with patch.dict(server.os.environ, {server.ACCESS_PASSWORD_ENV: "another-secret"}):
                message = server.seed_access_password(config)
            self.assertIn("忽略", message)
            self.assertNotIn("another-secret", message)
            self.assertEqual(access.load(path)["revision"], policy["revision"])
            with patch.dict(server.os.environ, {}, clear=False):
                server.os.environ.pop(server.ACCESS_PASSWORD_ENV, None)
                self.assertEqual(server.seed_access_password(config), "")