fix(register): align live sentinel flow with successful HAR
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
129
tests/test_register_live_sentinel.py
Normal file
129
tests/test_register_live_sentinel.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, *, status_code=200, text="", json_data=None, headers=None):
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
self._json_data = json_data
|
||||
self.headers = headers or {"content-type": "application/json"}
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
|
||||
class RegisterLiveSentinelTests(unittest.TestCase):
|
||||
def test_register_authorize_continue_and_so_token_are_used(self):
|
||||
from src.chatgpt_register_http_reverse import ChatGPTRegisterHTTPReverse
|
||||
|
||||
flow_history: list[str] = []
|
||||
def build_token(flow: str) -> str:
|
||||
flow_history.append(flow)
|
||||
return f"{flow}-token"
|
||||
|
||||
def build_session_observer_token(flow: str) -> str:
|
||||
flow_history.append("session_observer")
|
||||
return "session-observer-token"
|
||||
|
||||
fake_http = SimpleNamespace(
|
||||
request=Mock(return_value=FakeResponse(json_data={"accessToken": "at", "user": {"id": "uid"}})),
|
||||
session=SimpleNamespace(cookies=SimpleNamespace(jar=[])),
|
||||
)
|
||||
fake_mail = SimpleNamespace(
|
||||
create_mailbox=Mock(return_value={"address": "user@example.com", "id": "box-1", "password": ""}),
|
||||
wait_for_otp=Mock(return_value="123456"),
|
||||
)
|
||||
solver = SimpleNamespace(
|
||||
build_token=Mock(side_effect=build_token),
|
||||
build_session_observer_token=Mock(side_effect=build_session_observer_token),
|
||||
)
|
||||
|
||||
register = ChatGPTRegisterHTTPReverse(fake_http, fake_mail, sentinel_solver=solver)
|
||||
|
||||
order: list[str] = []
|
||||
create_call_args: dict[str, str | None] = {}
|
||||
|
||||
def fake_authorize_continue(*args, **kwargs):
|
||||
order.append("authorize_continue")
|
||||
return "https://auth.openai.com/create-account/password"
|
||||
|
||||
def fake_attempt_register(*args, **kwargs):
|
||||
order.append("attempt_register")
|
||||
return FakeResponse(status_code=200), {"continue_url": "continue-1"}
|
||||
|
||||
def fake_attempt_create_account(*args, sentinel=None, sentinel_so=None, **kwargs):
|
||||
order.append("attempt_create")
|
||||
create_call_args["sentinel"] = sentinel
|
||||
create_call_args["sentinel_so"] = sentinel_so
|
||||
return FakeResponse(status_code=200), {"continue_url": "continue-3"}
|
||||
|
||||
with patch.object(register, "_bootstrap_chatgpt", return_value="csrf"), \
|
||||
patch.object(register, "_signin_auth0", return_value="auth-url"), \
|
||||
patch.object(register, "_follow_auth_redirects", return_value="https://auth.openai.com/create-account/password"), \
|
||||
patch.object(register, "_accounts_api_base", return_value="https://auth.openai.com/api/accounts"), \
|
||||
patch.object(register, "_register_endpoint", return_value="https://auth.openai.com/api/accounts/user/register"), \
|
||||
patch.object(register, "_authorize_continue", side_effect=fake_authorize_continue, create=True) as authorize_continue, \
|
||||
patch.object(register, "_open_continue_url", side_effect=["email-url", "about-you-url"]), \
|
||||
patch.object(register, "_validate_otp", return_value="continue-2"), \
|
||||
patch.object(register, "_finalize_auth_callback"), \
|
||||
patch.object(register, "_attempt_register", side_effect=fake_attempt_register) as attempt_register, \
|
||||
patch.object(register, "_attempt_create_account", side_effect=fake_attempt_create_account) as attempt_create:
|
||||
result = register.register("Passw0rd!", "Jane Doe")
|
||||
|
||||
self.assertEqual(result["email"], "user@example.com")
|
||||
self.assertEqual(
|
||||
flow_history,
|
||||
[
|
||||
"authorize_continue",
|
||||
"username_password_create",
|
||||
"oauth_create_account",
|
||||
"session_observer",
|
||||
],
|
||||
)
|
||||
self.assertTrue(authorize_continue.called)
|
||||
self.assertEqual(authorize_continue.call_args.args[1], "https://auth.openai.com/log-in-or-create-account?usernameKind=email")
|
||||
self.assertLess(order.index("authorize_continue"), order.index("attempt_register"))
|
||||
self.assertEqual(attempt_register.call_args.args[1], "https://auth.openai.com/create-account/password")
|
||||
self.assertEqual(attempt_register.call_args.kwargs["sentinel"], "username_password_create-token")
|
||||
self.assertEqual(create_call_args["sentinel"], "oauth_create_account-token")
|
||||
self.assertEqual(create_call_args["sentinel_so"], "session-observer-token")
|
||||
solver.build_session_observer_token.assert_called_once_with("oauth_create_account")
|
||||
|
||||
|
||||
def test_authorize_continue_posts_screen_hint_and_json_headers(self):
|
||||
from src.chatgpt_register_http_reverse import ChatGPTRegisterHTTPReverse
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_request(method, url, **kwargs):
|
||||
captured['method'] = method
|
||||
captured['url'] = url
|
||||
captured['kwargs'] = kwargs
|
||||
return FakeResponse(status_code=200, json_data={"continue_url": "https://auth.openai.com/create-account/password"})
|
||||
|
||||
fake_http = SimpleNamespace(
|
||||
request=fake_request,
|
||||
session=SimpleNamespace(cookies=SimpleNamespace(jar=[])),
|
||||
)
|
||||
fake_mail = SimpleNamespace()
|
||||
register = ChatGPTRegisterHTTPReverse(fake_http, fake_mail, sentinel_solver=SimpleNamespace())
|
||||
|
||||
with patch.object(register, '_open_continue_url', return_value='https://auth.openai.com/create-account/password'):
|
||||
result = register._authorize_continue(
|
||||
'https://auth.openai.com/api/accounts',
|
||||
'https://auth.openai.com/log-in-or-create-account?usernameKind=email',
|
||||
'user@example.com',
|
||||
sentinel='authorize-token',
|
||||
)
|
||||
|
||||
self.assertEqual(result, 'https://auth.openai.com/create-account/password')
|
||||
self.assertEqual(captured['method'], 'POST')
|
||||
self.assertEqual(captured['kwargs']['json']['screen_hint'], 'login_or_signup')
|
||||
self.assertEqual(captured['kwargs']['headers']['Accept'], 'application/json')
|
||||
self.assertEqual(captured['kwargs']['headers']['openai-sentinel-token'], 'authorize-token')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
146
tests/test_sentinel_solver.py
Normal file
146
tests/test_sentinel_solver.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import json
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, *, text="", status_code=200, json_data=None, headers=None):
|
||||
self.text = text
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data
|
||||
self.headers = headers or {}
|
||||
|
||||
def json(self):
|
||||
if self._json_data is None:
|
||||
raise ValueError("no json")
|
||||
return self._json_data
|
||||
|
||||
|
||||
class FakeHTTP:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.device_id = "device-fallback-id"
|
||||
self.session = SimpleNamespace(
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 Test",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
},
|
||||
cookies=SimpleNamespace(
|
||||
jar=[SimpleNamespace(name="oai-did", value="cookie-did", domain="chatgpt.com")]
|
||||
),
|
||||
)
|
||||
self.req_payloads = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
self.calls.append((method, url, kwargs))
|
||||
if url.endswith("/backend-api/sentinel/sdk.js"):
|
||||
return FakeResponse(
|
||||
text=(
|
||||
"window.SentinelSDK = {};"
|
||||
"script.src = 'https://sentinel.openai.com/sentinel/20260219f9f6/sdk.js';"
|
||||
)
|
||||
)
|
||||
if "/backend-api/sentinel/frame.html?sv=" in url:
|
||||
return FakeResponse(text="<html></html>")
|
||||
if url.endswith("/backend-api/sentinel/req"):
|
||||
data = kwargs.get("data")
|
||||
if data:
|
||||
payload = json.loads(data)
|
||||
else:
|
||||
payload = {}
|
||||
self.req_payloads.append(payload)
|
||||
return FakeResponse(
|
||||
json_data={
|
||||
"token": "collector-token",
|
||||
"proofofwork": {"required": True, "seed": "seed-1", "difficulty": "0"},
|
||||
"turnstile": {"required": True, "dx": "encoded-dx"},
|
||||
"so": {"required": True, "snapshot_dx": "snapshot-dx"},
|
||||
}
|
||||
)
|
||||
raise AssertionError(f"unexpected request: {method} {url}")
|
||||
|
||||
|
||||
class SentinelSolverTests(unittest.TestCase):
|
||||
@patch("subprocess.run")
|
||||
def test_build_token_uses_enforcement_p_not_prepare_p(self, run_mock):
|
||||
from src.sentinel_solver import SentinelSolver
|
||||
|
||||
def fake_run(command, **kwargs):
|
||||
mode = command[-1]
|
||||
payload = json.loads(kwargs["input"])
|
||||
if mode == "prepare":
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "prepare-proof"}), stderr="")
|
||||
if mode == "turnstile":
|
||||
self.assertEqual(payload["p"], "prepare-proof")
|
||||
self.assertEqual(payload["dx"], "encoded-dx")
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"t": "wire-turnstile"}), stderr="")
|
||||
if mode == "enforcement":
|
||||
self.assertEqual(payload["chat_req"]["proofofwork"]["seed"], "seed-1")
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "enforcement-proof"}), stderr="")
|
||||
raise AssertionError(command)
|
||||
|
||||
run_mock.side_effect = fake_run
|
||||
http = FakeHTTP()
|
||||
solver = SentinelSolver(http)
|
||||
|
||||
token = solver.build_token("username_password_create")
|
||||
parsed = json.loads(token)
|
||||
|
||||
self.assertEqual(
|
||||
parsed,
|
||||
{
|
||||
"p": "enforcement-proof",
|
||||
"t": "wire-turnstile",
|
||||
"c": "collector-token",
|
||||
"id": "cookie-did",
|
||||
"flow": "username_password_create",
|
||||
},
|
||||
)
|
||||
self.assertEqual(http.calls[0][0:2], ("GET", "https://sentinel.openai.com/backend-api/sentinel/sdk.js"))
|
||||
self.assertTrue(http.calls[1][1].startswith("https://sentinel.openai.com/backend-api/sentinel/frame.html?sv="))
|
||||
self.assertEqual(http.calls[2][0:2], ("POST", "https://sentinel.openai.com/backend-api/sentinel/req"))
|
||||
self.assertEqual(http.req_payloads[-1]["p"], "prepare-proof")
|
||||
self.assertEqual(http.req_payloads[-1]["flow"], "username_password_create")
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_build_session_observer_token_uses_cached_chat_req_snapshot(self, run_mock):
|
||||
from src.sentinel_solver import SentinelSolver
|
||||
|
||||
def fake_run(command, **kwargs):
|
||||
mode = command[-1]
|
||||
payload = json.loads(kwargs["input"])
|
||||
if mode == "prepare":
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "prepare-proof"}), stderr="")
|
||||
if mode == "turnstile":
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"t": "wire-turnstile"}), stderr="")
|
||||
if mode == "enforcement":
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "enforcement-proof"}), stderr="")
|
||||
if mode == "session-observer":
|
||||
self.assertEqual(payload["flow"], "oauth_create_account")
|
||||
self.assertEqual(payload["c"], "collector-token")
|
||||
self.assertEqual(payload["snapshot_dx"], "snapshot-dx")
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps({"so": "wire-so"}), stderr="")
|
||||
raise AssertionError(command)
|
||||
|
||||
run_mock.side_effect = fake_run
|
||||
http = FakeHTTP()
|
||||
solver = SentinelSolver(http)
|
||||
|
||||
so_token = solver.build_session_observer_token("oauth_create_account")
|
||||
|
||||
self.assertEqual(
|
||||
json.loads(so_token),
|
||||
{
|
||||
"so": "wire-so",
|
||||
"c": "collector-token",
|
||||
"id": "cookie-did",
|
||||
"flow": "oauth_create_account",
|
||||
},
|
||||
)
|
||||
self.assertEqual(http.req_payloads[-1]["flow"], "oauth_create_account")
|
||||
self.assertEqual(http.req_payloads[-1]["p"], "prepare-proof")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
40
tests/test_yyds_mail_client.py
Normal file
40
tests/test_yyds_mail_client.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class YYDSMailClientDomainTests(unittest.TestCase):
|
||||
@patch('src.vmail_client.httpx.post')
|
||||
def test_create_mailbox_sends_configured_domain(self, post_mock):
|
||||
from src.vmail_client import YYDSMailClient, settings
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, headers=None, json=None, timeout=None):
|
||||
captured['url'] = url
|
||||
captured['headers'] = headers
|
||||
captured['json'] = json
|
||||
captured['timeout'] = timeout
|
||||
return SimpleNamespace(
|
||||
status_code=201,
|
||||
json=lambda: {
|
||||
'data': {
|
||||
'id': 'box-1',
|
||||
'address': 'chosen@20001028.xyz',
|
||||
'token': 'temp-token',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
post_mock.side_effect = fake_post
|
||||
|
||||
with patch.object(settings, 'yyds_mail_domain', '20001028.xyz', create=True):
|
||||
client = YYDSMailClient()
|
||||
mailbox = client.create_mailbox()
|
||||
|
||||
self.assertEqual(mailbox['address'], 'chosen@20001028.xyz')
|
||||
self.assertEqual(captured['json'], {'domain': '20001028.xyz'})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user