072d83da35
TDD GREEN phase - implementation passes all 8 tests: - ZhihuLoginResult: Pydantic model for login results - ZhihuLogin: Main class with QR code login flow - LOGIN_URL: https://www.zhihu.com/signin - CANVAS_SELECTORS: Multiple fallback selectors for QR canvas - _find_qrcode_canvas(): Find canvas with fallback selectors - _save_qrcode_image(): Save QR code to intermediate directory - check_login_cookie(): Detect z_c0 cookie for login state - get_cookies_str(): Extract cookie string from context - _wait_for_login(): Poll with configurable timeout (tenacity) - login_by_qrcode(): Main entry point for login flow Key design decisions: - Uses tenacity Retrying for configurable timeout - QR code saved to intermediate_path with timestamp - Multiple canvas selectors for robustness - Proper browser lifecycle management Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
4.9 KiB
Python
147 lines
4.9 KiB
Python
"""Tests for ZhihuLogin QR code login functionality
|
|
|
|
Tests the core login flow:
|
|
1. Access login page
|
|
2. Capture QR code screenshot
|
|
3. Poll for login cookie (z_c0)
|
|
4. Extract cookie string on success
|
|
5. Handle timeout
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch, AsyncMock
|
|
from pathlib import Path
|
|
|
|
|
|
class TestZhihuLogin:
|
|
"""Test suite for ZhihuLogin class."""
|
|
|
|
def test_login_page_access(self, mock_browser):
|
|
"""Test that login page is accessed with correct URL."""
|
|
from kb_workflow.auth import ZhihuLogin
|
|
|
|
login = ZhihuLogin(intermediate_path="/tmp/test")
|
|
login.browser = mock_browser
|
|
|
|
# Simulate calling goto
|
|
login.browser.goto("https://www.zhihu.com/signin")
|
|
|
|
# Verify goto was called with correct URL
|
|
mock_browser.goto.assert_called_once_with("https://www.zhihu.com/signin")
|
|
|
|
def test_qrcode_screenshot(self, mock_browser, mock_canvas, intermediate_path):
|
|
"""Test QR code screenshot is captured and saved correctly."""
|
|
from kb_workflow.auth import ZhihuLogin
|
|
|
|
login = ZhihuLogin(intermediate_path=intermediate_path)
|
|
login.browser = mock_browser
|
|
|
|
# Mock the canvas screenshot
|
|
mock_browser.query_selector.return_value = mock_canvas
|
|
mock_canvas.screenshot.return_value = b"fake_png_bytes"
|
|
|
|
# Call _save_qrcode_image
|
|
screenshot_bytes = b"fake_png_bytes"
|
|
qrcode_path = login._save_qrcode_image(screenshot_bytes)
|
|
|
|
# Verify file was created
|
|
assert qrcode_path.exists()
|
|
assert qrcode_path.suffix == ".png"
|
|
assert "zhihu_qrcode" in qrcode_path.name
|
|
|
|
def test_cookie_detection_with_z_c0(self, mock_browser, sample_cookies):
|
|
"""Test that login is detected when z_c0 cookie exists."""
|
|
from kb_workflow.auth import ZhihuLogin
|
|
|
|
login = ZhihuLogin(intermediate_path="/tmp/test")
|
|
login.browser = mock_browser
|
|
|
|
# Mock cookies to include z_c0
|
|
mock_browser.context.cookies.return_value = sample_cookies
|
|
|
|
# Check login cookie
|
|
is_logged_in = login.check_login_cookie()
|
|
|
|
assert is_logged_in is True
|
|
|
|
def test_cookie_detection_without_z_c0(self, mock_browser, sample_cookies_no_login):
|
|
"""Test that login is NOT detected when z_c0 cookie is missing."""
|
|
from kb_workflow.auth import ZhihuLogin
|
|
|
|
login = ZhihuLogin(intermediate_path="/tmp/test")
|
|
login.browser = mock_browser
|
|
|
|
# Mock cookies without z_c0
|
|
mock_browser.context.cookies.return_value = sample_cookies_no_login
|
|
|
|
# Check login cookie
|
|
is_logged_in = login.check_login_cookie()
|
|
|
|
assert is_logged_in is False
|
|
|
|
def test_cookie_extraction(self, mock_browser, sample_cookies):
|
|
"""Test cookie string extraction after successful login."""
|
|
from kb_workflow.auth import ZhihuLogin
|
|
|
|
login = ZhihuLogin(intermediate_path="/tmp/test")
|
|
login.browser = mock_browser
|
|
|
|
# Mock cookies
|
|
mock_browser.context.cookies.return_value = sample_cookies
|
|
|
|
# Get cookie string
|
|
cookie_str = login.get_cookies_str()
|
|
|
|
# Verify format: "name1=value1; name2=value2"
|
|
assert "z_c0=test_z_c0_value" in cookie_str
|
|
assert "_xsrf=test_xsrf_value" in cookie_str
|
|
assert ";" in cookie_str
|
|
|
|
def test_login_timeout(self, mock_browser, sample_cookies_no_login):
|
|
"""Test that exception is raised after timeout of polling."""
|
|
from kb_workflow.auth import ZhihuLogin
|
|
|
|
login = ZhihuLogin(intermediate_path="/tmp/test", timeout=2) # Short timeout for test
|
|
login.browser = mock_browser
|
|
|
|
# Mock cookies to always return no login
|
|
mock_browser.context.cookies.return_value = sample_cookies_no_login
|
|
|
|
# Should raise Exception after timeout (reraised by tenacity)
|
|
with pytest.raises(Exception, match="Not logged in yet"):
|
|
login._wait_for_login()
|
|
|
|
|
|
class TestZhihuLoginResult:
|
|
"""Test suite for ZhihuLoginResult model."""
|
|
|
|
def test_result_model_success(self):
|
|
"""Test ZhihuLoginResult model with successful login."""
|
|
from kb_workflow.auth import ZhihuLoginResult
|
|
|
|
result = ZhihuLoginResult(
|
|
success=True,
|
|
cookie_str="z_c0=abc123; _xsrf=xyz789",
|
|
qrcode_path=Path("/tmp/qrcode.png"),
|
|
error_message=None
|
|
)
|
|
|
|
assert result.success is True
|
|
assert "z_c0" in result.cookie_str
|
|
assert result.error_message is None
|
|
|
|
def test_result_model_failure(self):
|
|
"""Test ZhihuLoginResult model with failed login."""
|
|
from kb_workflow.auth import ZhihuLoginResult
|
|
|
|
result = ZhihuLoginResult(
|
|
success=False,
|
|
cookie_str="",
|
|
qrcode_path=Path("/tmp/qrcode.png"),
|
|
error_message="Login timeout after 60 seconds"
|
|
)
|
|
|
|
assert result.success is False
|
|
assert result.cookie_str == ""
|
|
assert "timeout" in result.error_message.lower()
|