File size: 4,013 Bytes
1140c43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"""完整诊断、分片流、持久状态和异常恢复出口的脱敏回归。"""
import asyncio
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "apps/market-api"))
from app.native.config import Settings
from app.native.operations import Operations
from app.native.store import Store
from app.native.supervisor import Supervisor


class RedactionTests(unittest.IsolatedAsyncioTestCase):
    def setUp(self):
        self.temporary = tempfile.TemporaryDirectory(prefix="message-redaction-")
        self.root = Path(self.temporary.name)
        self.settings = Settings(data_dir=self.root, admin_token="fixture-control-admin", mcp_tokens={"a": "fixture-control-mcp-a"}, allowed_hosts=("localhost",), allowed_origins=("http://localhost",), plugin_env_allowlist={"a": ["REDACTION_FIXTURE_SECRET"]})
        self.store = Store(self.root)
        self.supervisor = Supervisor(self.settings, self.store)
        self.secret = 'fixture-中文-quoted"-secret'

    def tearDown(self):
        self.store.close()
        self.temporary.cleanup()

    async def test_every_byte_boundary_is_redacted_after_drain(self):
        raw = self.secret.encode()
        with patch.dict(os.environ, {"REDACTION_FIXTURE_SECRET": self.secret}):
            for boundary in range(1, len(raw)):
                stream = asyncio.StreamReader()
                worker = SimpleNamespace(diagnostics="")
                draining = asyncio.create_task(self.supervisor.drain("a", worker, stream))
                stream.feed_data(raw[:boundary])
                await asyncio.sleep(0)
                stream.feed_data(raw[boundary:])
                stream.feed_eof()
                await draining
                self.assertEqual(worker.diagnostics, "[REDACTED]")

    async def test_store_and_error_reconciliation_have_final_boundary(self):
        with patch.dict(os.environ, {"REDACTION_FIXTURE_SECRET": self.secret}):
            self.store.save_profile({"id": "a", "last_error": self.secret})
            self.supervisor.mutate = AsyncMock(side_effect=RuntimeError("operation: " + self.secret))
            self.supervisor.refresh = AsyncMock(side_effect=RuntimeError("refresh: " + self.secret + " " + self.settings.admin_token))
            operations = Operations(self.store, self.supervisor)
            queued = operations.submit("a", "restart", {}, None)
            await operations.tasks[queued["id"]]
            completed = self.store.operation(queued["id"])
            self.assertEqual(completed["status"], "failed")
            self.assertNotIn(self.secret, json.dumps(completed, ensure_ascii=False))
            self.assertIn("[REDACTED]", completed["error"])
            self.assertIn("[REDACTED]", self.store.profile("a")["last_error"])
            # 即使未来调用点遗漏过滤,持久化入口也不能存入原始诊断值。
            completed.update(error=self.secret, result={"diagnostic": self.secret, "token": self.settings.mcp_tokens["a"]})
            self.store.save_operation(completed)
            for forbidden in (self.secret, self.settings.admin_token, self.settings.mcp_tokens["a"]):
                for path in self.root.glob("control.sqlite3*"):
                    self.assertNotIn(forbidden.encode(), path.read_bytes())

    async def test_cached_values_and_json_escaping_are_redacted(self):
        with patch.dict(os.environ, {"REDACTION_FIXTURE_SECRET": self.secret}):
            self.supervisor.redact("a", "")
        for value in (self.secret, json.dumps(self.secret, ensure_ascii=True)[1:-1], json.dumps(self.secret, ensure_ascii=False)[1:-1]):
            self.assertEqual(self.supervisor.redact("a", value), "[REDACTED]")
        self.assertEqual(self.supervisor.redact("b", self.secret), self.secret)
        self.assertEqual(self.supervisor.redact(None, self.secret), "[REDACTED]")