156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
"""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
|