"""Unit tests for browser cookie extraction (browser_cookie3 mocked).""" import sys import types from types import SimpleNamespace import pytest from src.browser_tokens import ( BrowserTokenError, extract_chatgpt_tokens, extract_claude_session, ) def _fake_bc3(monkeypatch, cookies_by_domain, raises=None): """Install a stub browser_cookie3 module whose ``brave`` loader returns SimpleNamespace cookies for the requested domain (or raises).""" module = types.ModuleType("browser_cookie3") def brave(domain_name=""): if raises is not None: raise raises return [ SimpleNamespace(name=name, value=value) for name, value in cookies_by_domain.get(domain_name, {}).items() ] module.brave = brave monkeypatch.setitem(sys.modules, "browser_cookie3", module) return module class TestExtractChatGPT: def test_chunked_cookies(self, monkeypatch): _fake_bc3(monkeypatch, { "chatgpt.com": { "__Secure-next-auth.session-token.0": "eyJpart0", "__Secure-next-auth.session-token.1": "part1rest", }, }) assert extract_chatgpt_tokens("brave") == ("eyJpart0", "part1rest") def test_single_cookie_fallback(self, monkeypatch): _fake_bc3(monkeypatch, { "chatgpt.com": {"__Secure-next-auth.session-token": "eyJsingle"}, }) assert extract_chatgpt_tokens("brave") == ("eyJsingle", None) def test_missing_cookie_raises_with_login_hint(self, monkeypatch): _fake_bc3(monkeypatch, {"chatgpt.com": {"unrelated": "x"}}) with pytest.raises(BrowserTokenError, match="log in"): extract_chatgpt_tokens("brave") def test_loader_failure_wrapped(self, monkeypatch): _fake_bc3(monkeypatch, {}, raises=RuntimeError("database is locked")) with pytest.raises(BrowserTokenError, match="Close the browser"): extract_chatgpt_tokens("brave") def test_unsupported_browser_rejected(self): with pytest.raises(BrowserTokenError, match="Unsupported browser"): extract_chatgpt_tokens("netscape") class TestExtractClaude: def test_session_key(self, monkeypatch): _fake_bc3(monkeypatch, {"claude.ai": {"sessionKey": "sk-ant-sid01-xyz"}}) assert extract_claude_session("brave") == "sk-ant-sid01-xyz" def test_missing_raises(self, monkeypatch): _fake_bc3(monkeypatch, {"claude.ai": {}}) with pytest.raises(BrowserTokenError, match="log in"): extract_claude_session("brave") class TestAuthFromBrowserCLI: def test_writes_env_after_validation(self, monkeypatch, tmp_path): """--from-browser extracts, validates live (mocked), and writes .env.""" from click.testing import CliRunner from src.main import cli from src.cache import Cache cache = Cache(tmp_path / "cache") cache.acknowledge_tos() _fake_bc3(monkeypatch, { "chatgpt.com": { "__Secure-next-auth.session-token.0": "eyJtok0", "__Secure-next-auth.session-token.1": "tok1", }, "claude.ai": {"sessionKey": "sk-ant-sid01-abc"}, }) # Stub out the live validation calls. import src.providers.chatgpt as chatgpt_mod import src.providers.claude as claude_mod monkeypatch.setattr( chatgpt_mod.ChatGPTProvider, "_fetch_access_token", lambda self: "at" ) monkeypatch.setattr( claude_mod.ClaudeProvider, "list_conversations", lambda self, offset=0, limit=100: [], ) runner = CliRunner(mix_stderr=True) with runner.isolated_filesystem(temp_dir=tmp_path): result = runner.invoke( cli, ["--no-log-file", "auth", "--from-browser"], env={"CACHE_DIR": str(tmp_path / "cache")}, ) assert result.exit_code == 0, result.output from pathlib import Path env_text = Path(".env").read_text() assert "CHATGPT_SESSION_TOKEN=eyJtok0" in env_text assert "CHATGPT_SESSION_TOKEN_1=tok1" in env_text assert "CLAUDE_SESSION_KEY=sk-ant-sid01-abc" in env_text assert "2 provider(s) configured" in result.output def test_validation_failure_leaves_env_untouched(self, monkeypatch, tmp_path): from click.testing import CliRunner from src.main import cli from src.cache import Cache from src.providers.base import ProviderError cache = Cache(tmp_path / "cache") cache.acknowledge_tos() _fake_bc3(monkeypatch, { "chatgpt.com": {"__Secure-next-auth.session-token.0": "eyJstale"}, "claude.ai": {}, }) import src.providers.chatgpt as chatgpt_mod def _fail(self): raise ProviderError("chatgpt", "auth", RuntimeError("401")) monkeypatch.setattr(chatgpt_mod.ChatGPTProvider, "_fetch_access_token", _fail) runner = CliRunner(mix_stderr=True) with runner.isolated_filesystem(temp_dir=tmp_path): result = runner.invoke( cli, ["--no-log-file", "auth", "--from-browser", "brave"], env={"CACHE_DIR": str(tmp_path / "cache")}, ) import pathlib env_exists = pathlib.Path(".env").exists() assert result.exit_code == 1 assert "failed live validation" in result.output assert not env_exists