test_chatgpt_flow.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import sys
  2. import types
  3. import unittest
  4. from unittest.mock import patch
  5. import chatgpt_flow
  6. from config import AppConfig
  7. class _FakeGeo:
  8. locale = "en-US"
  9. timezone_id = "America/New_York"
  10. class _FakePage:
  11. def on(self, *_args, **_kwargs):
  12. pass
  13. class _FakeBrowserContext:
  14. def new_page(self):
  15. return _FakePage()
  16. class _FakeBrowser:
  17. def new_context(self, **_kwargs):
  18. return _FakeBrowserContext()
  19. def close(self):
  20. pass
  21. class _FakeChromium:
  22. def launch(self, **_kwargs):
  23. return _FakeBrowser()
  24. class _FakePlaywright:
  25. chromium = _FakeChromium()
  26. class _FakeSyncPlaywright:
  27. def __enter__(self):
  28. return _FakePlaywright()
  29. def __exit__(self, *_args):
  30. return False
  31. def _fake_sync_playwright():
  32. return _FakeSyncPlaywright()
  33. class FullRunAttemptLabelTests(unittest.TestCase):
  34. def test_retry_keeps_account_number_based_on_success_count(self):
  35. calls = []
  36. def fake_run_one_account(_full_ctx, _page, idx, total):
  37. calls.append((idx, total))
  38. if len(calls) == 1:
  39. raise RuntimeError("signup failed")
  40. return {"stage": "cpa_uploaded", "email": "ok@example.com"}
  41. fake_patchright = types.ModuleType("patchright")
  42. fake_sync_api = types.ModuleType("patchright.sync_api")
  43. fake_sync_api.sync_playwright = _fake_sync_playwright
  44. fake_patchright.sync_api = fake_sync_api
  45. logs = []
  46. cfg = AppConfig(account_count=1, cpa_url="https://cpa.example", cpa_management_key="token", headless=True)
  47. with patch.dict(sys.modules, {"patchright": fake_patchright, "patchright.sync_api": fake_sync_api}):
  48. with patch("geo_fingerprint.detect_openai_geo_fingerprint", return_value=_FakeGeo()):
  49. with patch.object(chatgpt_flow, "_run_one_account", side_effect=fake_run_one_account):
  50. chatgpt_flow.run_full(cfg, log=logs.append)
  51. self.assertEqual(calls, [(1, 1), (1, 1)])
  52. self.assertTrue(any("启动账号 1/1 第2次尝试" in line for line in logs))
  53. class SsoCpaOauthRouteTests(unittest.TestCase):
  54. def test_full_sso_account_uses_cpa_oauth_instead_of_direct_session_upload(self):
  55. cfg = AppConfig(
  56. signup_mode="sso",
  57. cpa_url="https://cpa.example/management.html#/oauth",
  58. cpa_management_key="secret",
  59. )
  60. ctx = chatgpt_flow.FullRunContext(cfg=cfg, log=lambda *_: None)
  61. signup_session = {
  62. "accessToken": "tok",
  63. "account": {"planType": "free"},
  64. }
  65. oauth_result = {
  66. "fileName": "codex-oauth.json",
  67. "email": "n@example.com",
  68. "planType": "codex-oauth",
  69. "status": "Authentication successful!",
  70. }
  71. with patch.object(chatgpt_flow, "signup_chatgpt_sso", return_value={
  72. "email": "n@example.com",
  73. "password": "pw",
  74. "session": signup_session,
  75. }):
  76. with patch.object(chatgpt_flow, "upload_session_to_cpa") as direct_upload:
  77. with patch.object(chatgpt_flow, "authorize_codex_oauth_to_cpa", return_value=oauth_result, create=True) as oauth_upload:
  78. with patch.object(chatgpt_flow, "upsert_account"):
  79. with patch.object(chatgpt_flow, "add_event"):
  80. record = chatgpt_flow._run_one_account(ctx, _FakePage(), 1, 1)
  81. self.assertEqual(record["stage"], "cpa_uploaded")
  82. direct_upload.assert_not_called()
  83. oauth_upload.assert_called_once()
  84. def test_full_sso_account_does_not_require_session_access_token_before_cpa_oauth(self):
  85. cfg = AppConfig(
  86. signup_mode="sso",
  87. cpa_url="https://cpa.example/management.html#/oauth",
  88. cpa_management_key="secret",
  89. )
  90. ctx = chatgpt_flow.FullRunContext(cfg=cfg, log=lambda *_: None)
  91. oauth_result = {
  92. "fileName": "codex-oauth.json",
  93. "email": "n@example.com",
  94. "planType": "codex-oauth",
  95. "status": "Authentication successful!",
  96. }
  97. with patch.object(chatgpt_flow, "signup_chatgpt_sso", return_value={
  98. "email": "n@example.com",
  99. "password": "pw",
  100. "session": {},
  101. }):
  102. with patch.object(chatgpt_flow, "upload_session_to_cpa") as direct_upload:
  103. with patch.object(chatgpt_flow, "authorize_codex_oauth_to_cpa", return_value=oauth_result, create=True) as oauth_upload:
  104. with patch.object(chatgpt_flow, "upsert_account"):
  105. with patch.object(chatgpt_flow, "add_event"):
  106. record = chatgpt_flow._run_one_account(ctx, _FakePage(), 1, 1)
  107. self.assertEqual(record["stage"], "cpa_uploaded")
  108. self.assertEqual(record["planType"], "codex-oauth")
  109. direct_upload.assert_not_called()
  110. oauth_upload.assert_called_once()
  111. self.assertEqual(oauth_upload.call_args.kwargs["email_hint"], "n@example.com")
  112. def test_sso_batch_uses_cpa_oauth_instead_of_direct_session_upload(self):
  113. fake_patchright = types.ModuleType("patchright")
  114. fake_sync_api = types.ModuleType("patchright.sync_api")
  115. fake_sync_api.sync_playwright = _fake_sync_playwright
  116. fake_patchright.sync_api = fake_sync_api
  117. signup_session = {
  118. "accessToken": "tok",
  119. "account": {"planType": "free"},
  120. }
  121. oauth_result = {
  122. "fileName": "codex-oauth.json",
  123. "email": "n@example.com",
  124. "planType": "codex-oauth",
  125. "status": "Authentication successful!",
  126. }
  127. with patch.dict(sys.modules, {"patchright": fake_patchright, "patchright.sync_api": fake_sync_api}):
  128. with patch("geo_fingerprint.detect_openai_geo_fingerprint", return_value=_FakeGeo()):
  129. with patch.object(chatgpt_flow, "signup_chatgpt_sso", return_value={
  130. "email": "n@example.com",
  131. "password": "pw",
  132. "session": signup_session,
  133. }):
  134. with patch.object(chatgpt_flow, "upload_session_to_cpa") as direct_upload:
  135. with patch.object(chatgpt_flow, "authorize_codex_oauth_to_cpa", return_value=oauth_result, create=True) as oauth_upload:
  136. with patch.object(chatgpt_flow, "upsert_account"):
  137. with patch.object(chatgpt_flow, "add_event"):
  138. ctx = chatgpt_flow.run_sso_batch(
  139. account_count=1,
  140. cpa_url="https://cpa.example/management.html#/oauth",
  141. cpa_management_key="secret",
  142. headless=True,
  143. log=lambda *_: None,
  144. )
  145. self.assertEqual(ctx.accounts[0]["stage"], "cpa_uploaded")
  146. direct_upload.assert_not_called()
  147. oauth_upload.assert_called_once()
  148. self.assertEqual(oauth_upload.call_args.kwargs["cpa_url"], "https://cpa.example/management.html#/oauth")
  149. self.assertEqual(oauth_upload.call_args.kwargs["management_key"], "secret")
  150. def test_sso_batch_does_not_require_session_access_token_before_cpa_oauth(self):
  151. fake_patchright = types.ModuleType("patchright")
  152. fake_sync_api = types.ModuleType("patchright.sync_api")
  153. fake_sync_api.sync_playwright = _fake_sync_playwright
  154. fake_patchright.sync_api = fake_sync_api
  155. oauth_result = {
  156. "fileName": "codex-oauth.json",
  157. "email": "n@example.com",
  158. "planType": "codex-oauth",
  159. "status": "Authentication successful!",
  160. }
  161. with patch.dict(sys.modules, {"patchright": fake_patchright, "patchright.sync_api": fake_sync_api}):
  162. with patch("geo_fingerprint.detect_openai_geo_fingerprint", return_value=_FakeGeo()):
  163. with patch.object(chatgpt_flow, "signup_chatgpt_sso", return_value={
  164. "email": "n@example.com",
  165. "password": "pw",
  166. "session": {},
  167. }):
  168. with patch.object(chatgpt_flow, "upload_session_to_cpa") as direct_upload:
  169. with patch.object(chatgpt_flow, "authorize_codex_oauth_to_cpa", return_value=oauth_result, create=True) as oauth_upload:
  170. with patch.object(chatgpt_flow, "upsert_account"):
  171. with patch.object(chatgpt_flow, "add_event"):
  172. ctx = chatgpt_flow.run_sso_batch(
  173. account_count=1,
  174. cpa_url="https://cpa.example/management.html#/oauth",
  175. cpa_management_key="secret",
  176. headless=True,
  177. log=lambda *_: None,
  178. )
  179. self.assertEqual(ctx.accounts[0]["stage"], "cpa_uploaded")
  180. self.assertEqual(ctx.accounts[0]["planType"], "codex-oauth")
  181. direct_upload.assert_not_called()
  182. oauth_upload.assert_called_once()
  183. if __name__ == "__main__":
  184. unittest.main()