feat: v0.6.0 — collapse policy, session limiter, Claude Code provider, prune, browser auth, media downloads
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user