返回 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 )
47 assert json.loads(stdout.getvalue()) == {"ok": True}
48
49
50 def test_setup_without_cookie_flag_disables_browser_cookie_setup(monkeypatch):
51 with mock.patch.object(cli.env, "get_config", return_value={}), \
52 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
53 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
54 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
55 mock.patch.object(sys, "argv", ["last30days.py", "setup"]):
56 stdout = io.StringIO()
57 stderr = io.StringIO()
58 with redirect_stdout(stdout), redirect_stderr(stderr):
59 assert cli.main() == 0
60
61 assert setup.call_args.kwargs["allow_browser_cookies"] is False
62
63
64 def test_setup_cookie_flag_allows_browser_cookie_setup(monkeypatch):
65 with mock.patch.object(cli.env, "get_config", return_value={}), \
66 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
67 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
68 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
69 mock.patch.object(sys, "argv", ["last30days.py", "setup", "--allow-browser-cookies"]):
70 stdout = io.StringIO()
71 stderr = io.StringIO()
72 with redirect_stdout(stdout), redirect_stderr(stderr):
73 assert cli.main() == 0
74
75 assert setup.call_args.kwargs["allow_browser_cookies"] is True
76
77
78 def test_no_browser_cookies_overrides_setup_cookie_flag(monkeypatch):
79 seen: dict[str, object] = {}
80
81 def fake_get_config(*, policy):
82 seen["policy"] = policy
83 return {}
84
85 with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \
86 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
87 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
88 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
89 mock.patch.object(
90 sys,
91 "argv",
92 ["last30days.py", "--no-browser-cookies", "setup", "--allow-browser-cookies"],
93 ):
94 stdout = io.StringIO()
95 stderr = io.StringIO()
96 with redirect_stdout(stdout), redirect_stderr(stderr):
97 assert cli.main() == 0
98
99 assert seen["policy"].browser_cookies == "off"
100 assert setup.call_args.kwargs["allow_browser_cookies"] is False
101
102
103 def test_diagnose_overrides_setup_cookie_flag(monkeypatch):
104 seen: dict[str, object] = {}
105
106 def fake_get_config(*, policy):
107 seen["policy"] = policy
108 return {}
109
110 with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \
111 mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \
112 mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \
113 mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \
114 mock.patch.object(
115 sys,
116 "argv",
117 ["last30days.py", "--diagnose", "setup", "--allow-browser-cookies"],
118 ):
119 stdout = io.StringIO()
120 stderr = io.StringIO()
121 with redirect_stdout(stdout), redirect_stderr(stderr):
122 assert cli.main() == 0
123
124 assert seen["policy"].browser_cookies == "plan_only"
125 assert setup.call_args.kwargs["allow_browser_cookies"] is False
126
127
128 def test_research_run_defaults_to_browser_cookie_read():
129 """A plain research run reads cookies (the path that powers X auth)."""
130 parser = cli.build_parser()
131 args, extra = parser.parse_known_args(["some topic"])
132 policy = cli._config_policy_for_args(args, "some topic", extra)
133 assert policy.browser_cookies == "read"
134
135
136 def test_no_browser_cookies_flag_disables_research_run_cookie_read():
137 """--no-browser-cookies flips a research run to the no-read policy."""
138 parser = cli.build_parser()
139 args, extra = parser.parse_known_args(["--no-browser-cookies", "some topic"])
140 policy = cli._config_policy_for_args(args, "some topic", extra)
141 assert policy.browser_cookies == "off"
142
143
144 def test_watchlist_subprocess_disables_browser_cookies():
145 """The unattended watchlist cron must never probe browser cookies."""
146 import watchlist
147
148 fake_result = mock.Mock(returncode=1, stdout="", stderr="boom")
149 with mock.patch.object(watchlist, "store") as store, \
150 mock.patch.object(watchlist.subprocess, "run", return_value=fake_result) as run:
151 store.record_run.return_value = 1
152 watchlist._run_topic({"id": 1, "name": "test topic", "search_queries": None})
153
154 argv = run.call_args.args[0]
155 assert "--no-browser-cookies" in argv
156
157
158 def test_project_config_ignored_by_default_and_cannot_self_trust(tmp_path, monkeypatch):
159 project_env = tmp_path / ".claude" / "last30days.env"
160 project_env.parent.mkdir()
161 project_env.write_text(
162 "LAST30DAYS_TRUST_PROJECT_CONFIG=1\nOPENAI_BASE_URL=https://example.invalid\n",
163 encoding="utf-8",
164 )
165 monkeypatch.chdir(tmp_path)
166 monkeypatch.setattr(env, "CONFIG_FILE", None)
167 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
168 monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
169
170 with mock.patch.object(env, "_load_keychain", return_value={}), \
171 mock.patch.object(env, "_load_pass", return_value={}):
172 cfg = env.get_config()
173
174 assert cfg["OPENAI_BASE_URL"] is None
175 assert cfg["_CONFIG_SOURCE"] == "env_only"
176
177
178 def test_project_config_loads_with_process_trust_signal(tmp_path, monkeypatch):
179 project_env = tmp_path / ".claude" / "last30days.env"
180 project_env.parent.mkdir()
181 project_env.write_text("OPENAI_BASE_URL=https://trusted.example\n", encoding="utf-8")
182 monkeypatch.chdir(tmp_path)
183 monkeypatch.setattr(env, "CONFIG_FILE", None)
184 monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "1")
185 monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
186
187 with mock.patch.object(env, "_load_keychain", return_value={}), \
188 mock.patch.object(env, "_load_pass", return_value={}):
189 cfg = env.get_config()
190
191 assert cfg["OPENAI_BASE_URL"] == "https://trusted.example"
192 assert cfg["_CONFIG_SOURCE"].startswith(f"project:{project_env}")
193
193 lines PYTHON