返回 last30days-skill
test_chromium_browsers.py
根目录 / tests / test_chromium_browsers.py
1 """Tests for the extended Chromium-family browser cookie support.
2
3 Covers the three layers wired up for Brave/Edge/Vivaldi/Opera/Arc/Chromium:
4 - env.extract_browser_credentials (which browsers FROM_BROWSER selects)
5 - cookie_extract (routing browser name -> extractor)
6 - chrome_cookies (registry, profile finder, extraction)
7 """
8
9 import sqlite3
10 from unittest.mock import patch
11
12 import pytest
13
14 from lib.env import extract_browser_credentials
15 from lib.cookie_extract import extract_cookies
16 from lib.chrome_cookies import (
17 CHROMIUM_BROWSER_PROFILES,
18 _find_chromium_cookies_db,
19 extract_chromium_browser_cookies_macos,
20 )
21
22 # The Chromium-based browsers added on top of the original Chrome support.
23 NEW_CHROMIUM_BROWSERS = ["brave", "edge", "vivaldi", "opera", "arc", "chromium"]
24 ALL_AUTO_BROWSERS = ["firefox", "safari", "chrome", *NEW_CHROMIUM_BROWSERS]
25
26
27 def _base_config(**overrides):
28 cfg = {
29 "AUTH_TOKEN": None,
30 "CT0": None,
31 "TRUTHSOCIAL_TOKEN": None,
32 "FROM_BROWSER": None,
33 "SETUP_COMPLETE": None,
34 }
35 cfg.update(overrides)
36 return cfg
37
38
39 def _make_cookies_db(path, rows, db_version: int = 20) -> None:
40 """Create a minimal Chromium Cookies SQLite DB with plain (unencrypted) values."""
41 conn = sqlite3.connect(str(path))
42 c = conn.cursor()
43 c.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)")
44 c.execute("INSERT OR REPLACE INTO meta (key, value) VALUES ('version', ?)", (str(db_version),))
45 c.execute(
46 "CREATE TABLE cookies ("
47 " host_key TEXT NOT NULL,"
48 " name TEXT NOT NULL,"
49 " value TEXT NOT NULL DEFAULT '',"
50 " encrypted_value BLOB NOT NULL DEFAULT x''"
51 ")"
52 )
53 for host_key, name, value in rows:
54 c.execute(
55 "INSERT INTO cookies (host_key, name, value, encrypted_value) VALUES (?, ?, ?, ?)",
56 (host_key, name, value, b""),
57 )
58 conn.commit()
59 conn.close()
60
61
62 def _make_encrypted_cookies_db(path, rows, db_version: int = 24) -> None:
63 """Create a Cookies DB with v10-encrypted_value rows (empty value column)."""
64 conn = sqlite3.connect(str(path))
65 c = conn.cursor()
66 c.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)")
67 c.execute("INSERT OR REPLACE INTO meta (key, value) VALUES ('version', ?)", (str(db_version),))
68 c.execute(
69 "CREATE TABLE cookies ("
70 " host_key TEXT NOT NULL,"
71 " name TEXT NOT NULL,"
72 " value TEXT NOT NULL DEFAULT '',"
73 " encrypted_value BLOB NOT NULL DEFAULT x''"
74 ")"
75 )
76 for host_key, name, encrypted_value in rows:
77 c.execute(
78 "INSERT INTO cookies (host_key, name, value, encrypted_value) VALUES (?, ?, ?, ?)",
79 (host_key, name, "", encrypted_value),
80 )
81 conn.commit()
82 conn.close()
83
84
85 # ---------------------------------------------------------------------------
86 # env.py: FROM_BROWSER selects the right browsers
87 # ---------------------------------------------------------------------------
88
89
90 class TestEnvBrowserSelection:
91 @pytest.mark.parametrize("browser", NEW_CHROMIUM_BROWSERS)
92 @patch("lib.cookie_extract.extract_cookies")
93 def test_explicit_chromium_browser_is_used(self, mock_extract, browser):
94 """FROM_BROWSER=<chromium browser> routes extraction to that browser."""
95 mock_extract.return_value = {"auth_token": "tok", "ct0": "ct0val"}
96 config = _base_config(FROM_BROWSER=browser)
97
98 result = extract_browser_credentials(config)
99
100 assert result["AUTH_TOKEN"] == "tok"
101 assert result["CT0"] == "ct0val"
102 # Every extraction call targeted exactly the requested browser.
103 assert mock_extract.call_args_list
104 for call in mock_extract.call_args_list:
105 assert call[0][0] == browser
106
107 @patch("lib.cookie_extract.extract_cookies")
108 def test_auto_tries_every_chromium_browser(self, mock_extract):
109 """FROM_BROWSER=auto tries Firefox/Safari plus the whole Chromium family."""
110 mock_extract.return_value = None # force it to try them all
111 config = _base_config(FROM_BROWSER="auto")
112
113 extract_browser_credentials(config)
114
115 tried = {call[0][0] for call in mock_extract.call_args_list}
116 for browser in ALL_AUTO_BROWSERS:
117 assert browser in tried, f"auto should try {browser}"
118
119 @patch("lib.cookie_extract.extract_cookies")
120 def test_default_skips_browser_cookie_reads(self, mock_extract):
121 """Default (no FROM_BROWSER) reads no local browser cookies."""
122 mock_extract.return_value = None
123 config = _base_config()
124
125 extract_browser_credentials(config)
126
127 mock_extract.assert_not_called()
128
129
130 # ---------------------------------------------------------------------------
131 # cookie_extract.py: browser name routes to the chrome_cookies registry
132 # ---------------------------------------------------------------------------
133
134
135 class TestCookieExtractRouting:
136 @pytest.mark.parametrize("browser", ["edge", "vivaldi", "opera", "arc", "chromium"])
137 def test_routes_to_registry(self, browser):
138 with (
139 patch("lib.cookie_extract.platform.system", return_value="Darwin"),
140 patch(
141 "lib.chrome_cookies.extract_chromium_browser_cookies_macos",
142 return_value={"auth_token": f"{browser}_tok"},
143 ) as mock_macos,
144 ):
145 result = extract_cookies(browser, ".x.com", ["auth_token"])
146
147 assert result == {"auth_token": f"{browser}_tok"}
148 # The browser key is threaded through to the macOS extractor.
149 assert mock_macos.call_args[0][0] == browser
150
151 @pytest.mark.parametrize("browser", ["edge", "vivaldi", "opera", "arc", "chromium"])
152 def test_non_macos_returns_none(self, browser):
153 with patch("lib.cookie_extract.platform.system", return_value="Linux"):
154 assert extract_cookies(browser, ".x.com", ["auth_token"]) is None
155
156 def test_auto_macos_order_includes_chromium_family(self):
157 """auto on macOS calls every Chromium-family extractor when all miss."""
158 with (
159 patch("lib.cookie_extract.platform.system", return_value="Darwin"),
160 patch("lib.cookie_extract._extract_firefox_with_source", return_value=None),
161 patch("lib.cookie_extract.extract_chrome_cookies", return_value=None) as m_chrome,
162 patch("lib.cookie_extract.extract_brave_cookies", return_value=None) as m_brave,
163 patch("lib.cookie_extract.extract_edge_cookies", return_value=None) as m_edge,
164 patch("lib.cookie_extract.extract_vivaldi_cookies", return_value=None) as m_viv,
165 patch("lib.cookie_extract.extract_opera_cookies", return_value=None) as m_opera,
166 patch("lib.cookie_extract.extract_arc_cookies", return_value=None) as m_arc,
167 patch("lib.cookie_extract.extract_chromium_cookies", return_value=None) as m_chr,
168 patch("lib.cookie_extract.extract_safari_cookies", return_value=None),
169 ):
170 result = extract_cookies("auto", ".x.com", ["auth_token"])
171
172 assert result is None
173 for mock_fn in (m_chrome, m_brave, m_edge, m_viv, m_opera, m_arc, m_chr):
174 mock_fn.assert_called_once_with(".x.com", ["auth_token"])
175
176
177 # ---------------------------------------------------------------------------
178 # chrome_cookies.py: registry, profile finder, generic extraction
179 # ---------------------------------------------------------------------------
180
181
182 class TestChromiumRegistry:
183 def test_registry_has_expected_browsers(self):
184 assert set(CHROMIUM_BROWSER_PROFILES) == {"edge", "vivaldi", "opera", "arc", "chromium"}
185 for base_dir, service in CHROMIUM_BROWSER_PROFILES.values():
186 assert service.endswith("Safe Storage")
187 assert base_dir is not None
188
189 def test_unknown_browser_returns_none(self):
190 assert extract_chromium_browser_cookies_macos("netscape", ".x.com", ["auth_token"]) is None
191
192 def test_generic_extraction_plain_values(self, tmp_path):
193 """A registry browser extracts unencrypted cookies via the shared core."""
194 base = tmp_path / "Edge"
195 (base / "Default").mkdir(parents=True)
196 _make_cookies_db(
197 base / "Default" / "Cookies",
198 [
199 (".x.com", "auth_token", "edge_auth"),
200 (".x.com", "ct0", "edge_ct0"),
201 (".other.com", "session", "nope"),
202 ],
203 )
204
205 with (
206 patch.dict(
207 "lib.chrome_cookies.CHROMIUM_BROWSER_PROFILES",
208 {"edge": (base, "Microsoft Edge Safe Storage")},
209 ),
210 # Plain values need no Keychain; ensure we never prompt.
211 patch("lib.chrome_cookies._get_chromium_encryption_key", return_value=None),
212 ):
213 result = extract_chromium_browser_cookies_macos("edge", ".x.com", ["auth_token", "ct0"])
214
215 assert result == {"auth_token": "edge_auth", "ct0": "edge_ct0"}
216
217 def test_db_not_found_returns_none(self, tmp_path):
218 empty = tmp_path / "Vivaldi"
219 empty.mkdir()
220 with patch.dict(
221 "lib.chrome_cookies.CHROMIUM_BROWSER_PROFILES",
222 {"vivaldi": (empty, "Vivaldi Safe Storage")},
223 ):
224 assert extract_chromium_browser_cookies_macos("vivaldi", ".x.com", ["auth_token"]) is None
225
226
227 class TestFindChromiumCookiesDb:
228 def test_prefers_default_profile(self, tmp_path):
229 (tmp_path / "Default").mkdir()
230 default_db = tmp_path / "Default" / "Cookies"
231 default_db.touch()
232 (tmp_path / "Cookies").touch() # direct file should be ignored
233 assert _find_chromium_cookies_db(tmp_path) == default_db
234
235 def test_falls_back_to_direct_cookies(self, tmp_path):
236 """Opera-style layout: Cookies directly under the base dir."""
237 direct = tmp_path / "Cookies"
238 direct.touch()
239 assert _find_chromium_cookies_db(tmp_path) == direct
240
241 def test_falls_back_to_numbered_profile(self, tmp_path):
242 prof = tmp_path / "Profile 2"
243 prof.mkdir()
244 db = prof / "Cookies"
245 db.touch()
246 assert _find_chromium_cookies_db(tmp_path) == db
247
248 def test_returns_none_when_missing(self, tmp_path):
249 assert _find_chromium_cookies_db(tmp_path) is None
250
251 def test_prefers_network_cookies_over_flat(self, tmp_path):
252 """Modern Chromium (>=96) stores under Default/Network/Cookies."""
253 (tmp_path / "Default" / "Network").mkdir(parents=True)
254 net = tmp_path / "Default" / "Network" / "Cookies"
255 net.touch()
256 (tmp_path / "Default" / "Cookies").touch() # legacy flat also present
257 assert _find_chromium_cookies_db(tmp_path) == net
258
259 def test_network_cookies_in_numbered_profile(self, tmp_path):
260 (tmp_path / "Profile 1" / "Network").mkdir(parents=True)
261 net = tmp_path / "Profile 1" / "Network" / "Cookies"
262 net.touch()
263 assert _find_chromium_cookies_db(tmp_path) == net
264
265
266 class TestLazyKeychain:
267 """The Keychain key is fetched only when an encrypted cookie must be decrypted.
268
269 This keeps FROM_BROWSER=auto from prompting for every installed Chromium
270 browser - only the one actually holding the requested cookie prompts.
271 """
272
273 def _edge_at(self, tmp_path, rows, encrypted=False):
274 base = tmp_path / "Edge"
275 (base / "Default").mkdir(parents=True)
276 db = base / "Default" / "Cookies"
277 if encrypted:
278 _make_encrypted_cookies_db(db, rows)
279 else:
280 _make_cookies_db(db, rows)
281 return base
282
283 def test_keychain_not_fetched_for_plain_values(self, tmp_path):
284 base = self._edge_at(tmp_path, [(".x.com", "auth_token", "plain_tok")])
285 with (
286 patch.dict("lib.chrome_cookies.CHROMIUM_BROWSER_PROFILES",
287 {"edge": (base, "Microsoft Edge Safe Storage")}),
288 patch("lib.chrome_cookies._get_chromium_encryption_key") as key_mock,
289 ):
290 result = extract_chromium_browser_cookies_macos("edge", ".x.com", ["auth_token"])
291 assert result == {"auth_token": "plain_tok"}
292 key_mock.assert_not_called() # no decryption needed -> no Keychain prompt
293
294 def test_keychain_not_fetched_when_no_match(self, tmp_path):
295 base = self._edge_at(tmp_path, [(".other.com", "auth_token", "x")])
296 with (
297 patch.dict("lib.chrome_cookies.CHROMIUM_BROWSER_PROFILES",
298 {"edge": (base, "Microsoft Edge Safe Storage")}),
299 patch("lib.chrome_cookies._get_chromium_encryption_key") as key_mock,
300 ):
301 result = extract_chromium_browser_cookies_macos("edge", ".x.com", ["auth_token"])
302 assert result is None
303 key_mock.assert_not_called() # cookie absent -> no Keychain prompt
304
305 def test_keychain_fetched_and_decrypts_v10(self, tmp_path):
306 base = self._edge_at(tmp_path, [(".x.com", "auth_token", b"v10ciphertextbytes")], encrypted=True)
307 with (
308 patch.dict("lib.chrome_cookies.CHROMIUM_BROWSER_PROFILES",
309 {"edge": (base, "Microsoft Edge Safe Storage")}),
310 patch("lib.chrome_cookies._get_chromium_encryption_key", return_value=b"passphrase") as key_mock,
311 patch("lib.chrome_cookies._decrypt_v10_value", return_value="decrypted_tok") as dec_mock,
312 ):
313 result = extract_chromium_browser_cookies_macos("edge", ".x.com", ["auth_token"])
314 assert result == {"auth_token": "decrypted_tok"}
315 key_mock.assert_called_once_with("Microsoft Edge Safe Storage")
316 assert dec_mock.called
317
317 lines PYTHON