Ctrl K
ComunidadGuías…

tests/test_auth.py

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

Archivo original ↗Preguntar a KiBot
"""口令的生成、存放与取用,以及绑局域网时的启动闸门。

闸门本身(`require_auth` 三兄弟怎么判、401 有几种形态)在 `test_fastapi_api.py`;
这里只管「这台机器有没有口令、`peach serve` 该不该起得来」。
"""
from __future__ import annotations

import io
import os
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock

from peach import auth, cli, settings_file


class TokenFileTests(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.secrets = Path(self.tmp.name).resolve() / "secrets"

    def test_a_missing_file_reads_as_no_token(self):
        self.assertEqual(auth.read_token(self.secrets), "")

    def test_a_blank_file_reads_as_no_token(self):
        self.secrets.mkdir(parents=True)
        auth.token_path(self.secrets).write_text("  \n", encoding="utf-8")
        self.assertEqual(auth.read_token(self.secrets), "")

    def test_a_new_token_carries_at_least_256_bit(self):
        token = auth.write_token(self.secrets)
        # token_urlsafe(32) 出 43 个字符;断言字符数而不是熵,是因为长度是这里唯一
        # 能直接观察的量,而它由 TOKEN_BYTES 唯一决定。
        self.assertGreaterEqual(len(token), 43)
        self.assertTrue(all(c.isalnum() or c in "-_" for c in token), token)
        self.assertEqual(auth.read_token(self.secrets), token)

    def test_ensure_keeps_the_token_already_on_disk(self):
        first, created = auth.ensure_token(self.secrets)
        self.assertTrue(created)
        second, created_again = auth.ensure_token(self.secrets)
        self.assertEqual(second, first)
        self.assertFalse(created_again)

    def test_writing_again_replaces_the_token(self):
        first = auth.write_token(self.secrets)
        self.assertNotEqual(auth.write_token(self.secrets), first)

    @unittest.skipIf(os.name == "nt", "Windows 的 chmod 只动只读位,权限靠 Peach 数据目录本身")
    def test_the_token_file_is_owner_only(self):
        auth.write_token(self.secrets)
        self.assertEqual(auth.token_path(self.secrets).stat().st_mode & 0o777, 0o600)

    def test_resolution_order_is_flag_then_environment_then_file(self):
        auth.write_token(self.secrets)
        on_disk = auth.read_token(self.secrets)
        with mock.patch.dict(os.environ, {"PEACH_TOKEN": "from-env"}):
            self.assertEqual(auth.resolve_token("from-flag", self.secrets), "from-flag")
            self.assertEqual(auth.resolve_token("", self.secrets), "from-env")
        with mock.patch.dict(os.environ, {}, clear=True):
            self.assertEqual(auth.resolve_token("", self.secrets), on_disk)


class ServeGateTests(unittest.TestCase):
    """绑局域网地址却没有口令时,`peach serve` 必须拒绝启动而不是告警。"""

    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.root = Path(self.tmp.name).resolve() / "peach-data"
        self.config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(self.root)})
        patcher = mock.patch.object(cli.settings_file, "active", return_value=self.config)
        patcher.start()
        self.addCleanup(patcher.stop)
        env = mock.patch.dict(os.environ, {}, clear=True)
        env.start()
        self.addCleanup(env.stop)

    def _args(self, *extra):
        return cli.build_parser().parse_args(["serve", *extra])

    def _secrets(self) -> Path:
        return self.config.directory("secrets")

    def test_a_lan_bind_without_a_token_refuses_to_start(self):
        with self.assertRaises(SystemExit) as raised:
            cli._serve_token(self._args("--host", "0.0.0.0"))
        message = str(raised.exception)
        self.assertIn("0.0.0.0", message)
        self.assertIn("peach token", message)
        # 拒绝的时候不许顺手把口令生成掉:那等于把硬门槛降成一句提示。
        self.assertFalse(auth.token_path(self._secrets()).exists())

    def test_a_loopback_bind_runs_without_a_token(self):
        self.assertEqual(cli._serve_token(self._args("--host", "127.0.0.1")), "")
        self.assertEqual(cli._serve_token(self._args("--host", "localhost")), "")

    def test_a_lan_bind_takes_the_token_from_the_file(self):
        token = auth.write_token(self._secrets())
        self.assertEqual(cli._serve_token(self._args("--host", "0.0.0.0")), token)

    def test_an_explicit_flag_still_wins(self):
        auth.write_token(self._secrets())
        self.assertEqual(
            cli._serve_token(self._args("--host", "0.0.0.0", "--token", "given")), "given")

    def test_no_auth_skips_the_login_page_on_loopback_and_leaves_the_token_file_alone(self):
        token = auth.write_token(self._secrets())
        self.assertEqual(cli._serve_token(self._args("--host", "127.0.0.1", "--no-auth")), "")
        # 口令文件是托盘那份服务的门槛,开发时跳过登录不该把它擦掉或改写。
        self.assertEqual(auth.read_token(self._secrets()), token)

    def test_no_auth_refuses_a_lan_bind_even_when_a_token_exists(self):
        auth.write_token(self._secrets())
        with self.assertRaises(SystemExit) as raised:
            cli._serve_token(self._args("--host", "0.0.0.0", "--no-auth"))
        self.assertIn("0.0.0.0", str(raised.exception))


class TokenCommandTests(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.root = Path(self.tmp.name).resolve() / "peach-data"
        self.config = settings_file.load_config(environ={"PEACH_DATA_ROOT": str(self.root)})
        patcher = mock.patch.object(cli.settings_file, "active", return_value=self.config)
        patcher.start()
        self.addCleanup(patcher.stop)

    def _run(self, *extra):
        output = io.StringIO()
        with redirect_stdout(output):
            code = cli.main(["token", *extra])
        return code, output.getvalue()

    def test_it_creates_and_prints_a_token(self):
        code, output = self._run()
        self.assertEqual(code, 0)
        token = auth.read_token(self.config.directory("secrets"))
        self.assertTrue(token)
        self.assertIn(token, output)

    def test_reading_twice_gives_the_same_token(self):
        self._run()
        token = auth.read_token(self.config.directory("secrets"))
        _code, again = self._run()
        self.assertIn(token, again)
        self.assertEqual(auth.read_token(self.config.directory("secrets")), token)

    def test_rotate_replaces_the_token(self):
        self._run()
        before = auth.read_token(self.config.directory("secrets"))
        _code, output = self._run("--rotate")
        after = auth.read_token(self.config.directory("secrets"))
        self.assertNotEqual(after, before)
        self.assertIn(after, output)


class InitProvisionsTokenTests(unittest.TestCase):
    """两条 init 路径都要留下口令文件——现有部署走的正是 `--from-existing`。"""

    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.root = Path(self.tmp.name).resolve() / "peach-data"
        patcher = mock.patch.object(cli.onboarding.certs, "bootstrap_certificates")
        patcher.start()
        self.addCleanup(patcher.stop)

    def _init(self, *extra):
        output = io.StringIO()
        with redirect_stdout(output):
            code = cli.main(["init", "--data-root", str(self.root), *extra])
        return code, output.getvalue()

    def test_a_fresh_init_leaves_a_token_behind(self):
        code, output = self._init()
        self.assertEqual(code, 0)
        token = auth.read_token(self.root / "secrets")
        self.assertTrue(token)
        # 口令本身不进终端输出:那条路通向日志、截图和 issue。
        self.assertNotIn(token, output)
        self.assertIn("peach token", output)

    def test_from_existing_leaves_a_token_behind(self):
        code, _output = self._init("--from-existing")
        self.assertEqual(code, 0)
        self.assertTrue(auth.read_token(self.root / "secrets"))


if __name__ == "__main__":
    unittest.main()