返回 last30days-skill
test_security_boundaries.py
根目录 / tests / test_security_boundaries.py
1 """Regression tests for agent-host local-read boundaries."""
2
3 from __future__ import annotations
4
5 import importlib
6 import io
7 import json
8 import os
9 import sys
10 from contextlib import redirect_stderr, redirect_stdout
11 from unittest import mock
12
13 import last30days as cli
14 from lib import env
15
16
17 def test_importing_cli_does_not_load_config_or_propagate_endpoints(monkeypatch):
18 monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
19 monkeypatch.delenv("XAI_BASE_URL", raising=False)
20 with mock.patch("lib.env.get_config", side_effect=AssertionError("import loaded config")):
21 importlib.reload(cli)
22 assert os.environ.get("OPENAI_BASE_URL") is None
23 assert os.environ.get("XAI_BASE_URL") is None
24
25
26 def test_diagnose_uses_plan_only_cookie_policy_and_safe_pipeline(monkeypatch):
27 seen: dict[str, object] = {}
28
29 def fake_get_config(*, policy):
30 seen["policy"] = policy
31 return {"_BROWSER_COOKIE_MODE": policy.browser_cookies, "_BROWSER_COOKIE_BROWSERS": ["firefox"]}
32
33 with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \
34 mock.patch.object(cli.pipeline, "diagnose", return_value={"ok": True}) as diagnose, \
35 mock.patch.object(sys, "argv", ["last30days.py", "--diagnose"]):
36 stdout = io.StringIO()
37 stderr = io.StringIO()
38 with redirect_stdout(stdout), redirect_stderr(stderr):
39 assert cli.main() == 0
40
41 assert seen["policy"].browser_cookies == "plan_only"
42 diagnose.assert_called_once_with(
43 {"_BROWSER_COOKIE_MODE": "plan_only", "_BROWSER_COOKIE_BROWSERS": ["firefox"]},
44 None,
45 safe=True,
46 x_envelope=False,
47 )
48 assert json.loads(stdout.getvalue()) == {"ok": True}
49
50
51 def test_setup_without_cookie_flag_disables_browser_cookie_setup(monkeypatch):
52 with mock.patch.object(cli.env, "get_config", return_value={}), \
53 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
54 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
55 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
56 mock.patch.object(sys, "argv", ["last30days.py", "setup"]):
57 stdout = io.StringIO()
58 stderr = io.StringIO()
59 with redirect_stdout(stdout), redirect_stderr(stderr):
60 assert cli.main() == 0
61
62 assert setup.call_args.kwargs["allow_browser_cookies"] is False
63
64
65 def test_setup_cookie_flag_allows_browser_cookie_setup(monkeypatch):
66 with mock.patch.object(cli.env, "get_config", return_value={}), \
67 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
68 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
69 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
70 mock.patch.object(sys, "argv", ["last30days.py", "setup", "--allow-browser-cookies"]):
71 stdout = io.StringIO()
72 stderr = io.StringIO()
73 with redirect_stdout(stdout), redirect_stderr(stderr):
74 assert cli.main() == 0
75
76 assert setup.call_args.kwargs["allow_browser_cookies"] is True
77
78
79 def test_no_browser_cookies_overrides_setup_cookie_flag(monkeypatch):
80 seen: dict[str, object] = {}
81
82 def fake_get_config(*, policy):
83 seen["policy"] = policy
84 return {}
85
86 with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \
87 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
88 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
89 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
90 mock.patch.object(
91 sys,
92 "argv",
93 ["last30days.py", "--no-browser-cookies", "setup", "--allow-browser-cookies"],
94 ):
95 stdout = io.StringIO()
96 stderr = io.StringIO()
97 with redirect_stdout(stdout), redirect_stderr(stderr):
98 assert cli.main() == 0
99
100 assert seen["policy"].browser_cookies == "off"
101 assert setup.call_args.kwargs["allow_browser_cookies"] is False
102
103
104 def test_diagnose_overrides_setup_cookie_flag(monkeypatch):
105 seen: dict[str, object] = {}
106
107 def fake_get_config(*, policy):
108 seen["policy"] = policy
109 return {}
110
111 with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \
112 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
113 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
114 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
115 mock.patch.object(
116 sys,
117 "argv",
118 ["last30days.py", "--diagnose", "setup", "--allow-browser-cookies"],
119 ):
120 stdout = io.StringIO()
121 stderr = io.StringIO()
122 with redirect_stdout(stdout), redirect_stderr(stderr):
123 assert cli.main() == 0
124
125 assert seen["policy"].browser_cookies == "plan_only"
126 assert setup.call_args.kwargs["allow_browser_cookies"] is False
127
128
129 def test_research_run_defaults_to_browser_cookie_read():
130 """A plain research run reads cookies (the path that powers X auth)."""
131 parser = cli.build_parser()
132 args, extra = parser.parse_known_args(["some topic"])
133 policy = cli._config_policy_for_args(args, "some topic", extra)
134 assert policy.browser_cookies == "read"
135
136
137 def test_no_browser_cookies_flag_disables_research_run_cookie_read():
138 """--no-browser-cookies flips a research run to the no-read policy."""
139 parser = cli.build_parser()
140 args, extra = parser.parse_known_args(["--no-browser-cookies", "some topic"])
141 policy = cli._config_policy_for_args(args, "some topic", extra)
142 assert policy.browser_cookies == "off"
143
144
145 def test_watchlist_subprocess_disables_browser_cookies():
146 """The unattended watchlist cron must never probe browser cookies."""
147 import watchlist
148
149 fake_result = mock.Mock(returncode=1, stdout="", stderr="boom")
150 with mock.patch.object(watchlist, "store") as store, \
151 mock.patch.object(watchlist.subprocess, "run", return_value=fake_result) as run:
152 store.record_run.return_value = 1
153 watchlist._run_topic({"id": 1, "name": "test topic", "search_queries": None})
154
155 argv = run.call_args.args[0]
156 assert "--no-browser-cookies" in argv
157
158
159 def test_project_config_ignored_by_default_and_cannot_self_trust(tmp_path, monkeypatch):
160 project_env = tmp_path / ".claude" / "last30days.env"
161 project_env.parent.mkdir()
162 project_env.write_text(
163 "LAST30DAYS_TRUST_PROJECT_CONFIG=1\nOPENAI_BASE_URL=https://example.invalid\n",
164 encoding="utf-8",
165 )
166 monkeypatch.chdir(tmp_path)
167 monkeypatch.setattr(env, "CONFIG_FILE", None)
168 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
169 monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
170
171 with mock.patch.object(env, "_load_keychain", return_value={}), \
172 mock.patch.object(env, "_load_pass", return_value={}):
173 cfg = env.get_config()
174
175 assert cfg["OPENAI_BASE_URL"] is None
176 assert cfg["_CONFIG_SOURCE"] == "env_only"
177
178
179 def test_project_config_loads_with_process_trust_signal(tmp_path, monkeypatch):
180 project_env = tmp_path / ".claude" / "last30days.env"
181 project_env.parent.mkdir()
182 project_env.write_text("OPENAI_BASE_URL=https://trusted.example\n", encoding="utf-8")
183 monkeypatch.chdir(tmp_path)
184 monkeypatch.setattr(env, "CONFIG_FILE", None)
185 monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "1")
186 monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
187
188 with mock.patch.object(env, "_load_keychain", return_value={}), \
189 mock.patch.object(env, "_load_pass", return_value={}):
190 cfg = env.get_config()
191
192 assert cfg["OPENAI_BASE_URL"] == "https://trusted.example"
193 assert cfg["_CONFIG_SOURCE"].startswith(f"project:{project_env}")
194
194 lines PYTHON