mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-11 01:50:13 +08:00
Add Codex Auth support in account management so selected accounts can complete a Codex-compatible OAuth login flow and export usable auth.json files. This commit includes: - account-management UI entrypoints for Codex Auth login and auth.json download - backend SSE routes for single-account and batch Codex Auth login execution - persistence of freshly returned Codex-compatible tokens back into the account database - Codex auth export support for direct auth.json download and batch zip packaging - tests covering the Codex Auth login flow and export behavior The OTP verification failure was caused by manually sending a second OTP after password verification. The flow now reuses the existing proven login path: login re-entry, password verification, automatic OTP reception, consent page handling, workspace selection, and OAuth callback exchange. Successful logins now also persist workspace_id together with the refreshed Codex-compatible tokens, making later re-export of auth.json possible without requiring the browser-downloaded file to still exist locally. Change-Id: I59df518ef4dc05f8bc52c734dd1b738fcb0b7a4e
150 lines
5.0 KiB
Python
150 lines
5.0 KiB
Python
from types import SimpleNamespace
|
|
|
|
import src.core.codex_auth as codex_auth_module
|
|
from src.core.codex_auth import CodexAuthEngine
|
|
from src.core.register import PhaseResult, RegistrationEngine
|
|
from src.services import EmailServiceType
|
|
|
|
|
|
class DummySettings:
|
|
openai_client_id = "client-id"
|
|
openai_auth_url = "https://auth.example.test/oauth/authorize"
|
|
openai_token_url = "https://auth.example.test/oauth/token"
|
|
|
|
|
|
class FakeEmailService:
|
|
service_type = EmailServiceType.TEMPMAIL
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, *, status_code=200, url="", text=""):
|
|
self.status_code = status_code
|
|
self.url = url
|
|
self.text = text
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self, response):
|
|
self.response = response
|
|
self.calls = []
|
|
|
|
def get(self, url, **kwargs):
|
|
self.calls.append({"url": url, "kwargs": kwargs})
|
|
return self.response
|
|
|
|
|
|
def _build_engine(monkeypatch):
|
|
monkeypatch.setattr(codex_auth_module, "get_settings", lambda: DummySettings())
|
|
return CodexAuthEngine(
|
|
email="tester@example.com",
|
|
password="Pass12345",
|
|
email_service=FakeEmailService(),
|
|
email_service_id="svc-1",
|
|
)
|
|
|
|
|
|
def test_codex_auth_run_reuses_working_login_flow_without_manual_otp_send(monkeypatch):
|
|
engine = _build_engine(monkeypatch)
|
|
|
|
def fake_start_oauth(self):
|
|
self.oauth_start = SimpleNamespace(
|
|
auth_url="https://auth.example.test/oauth/authorize",
|
|
state="state-1",
|
|
code_verifier="verifier-1",
|
|
)
|
|
return True
|
|
|
|
monkeypatch.setattr(RegistrationEngine, "_init_session", lambda self: True)
|
|
monkeypatch.setattr(RegistrationEngine, "_start_oauth", fake_start_oauth)
|
|
monkeypatch.setattr(RegistrationEngine, "_get_device_id", lambda self: "did-1")
|
|
monkeypatch.setattr(engine, "_try_reenter_login_flow", lambda: True)
|
|
monkeypatch.setattr(
|
|
engine,
|
|
"_send_verification_code",
|
|
lambda: (_ for _ in ()).throw(AssertionError("unexpected manual otp send")),
|
|
raising=False,
|
|
)
|
|
|
|
seen = {}
|
|
monkeypatch.setattr(codex_auth_module.time, "time", lambda: 1_700_000_000.0)
|
|
|
|
def fake_submit_login_password_step():
|
|
seen["anchor_before_password"] = engine._otp_sent_at
|
|
return True
|
|
|
|
def fake_phase_otp_secondary(context, started_at=None):
|
|
seen["anchor_before_wait"] = context.otp_sent_at
|
|
seen["otp_wait_started_at"] = started_at
|
|
return "654321", PhaseResult(phase="otp_secondary", success=True)
|
|
|
|
monkeypatch.setattr(engine, "_submit_login_password_step", fake_submit_login_password_step)
|
|
monkeypatch.setattr(engine, "_phase_otp_secondary", fake_phase_otp_secondary)
|
|
monkeypatch.setattr(
|
|
engine,
|
|
"_validate_verification_code_and_get_continue_url",
|
|
lambda code: (True, "https://auth.example.test/consent"),
|
|
)
|
|
monkeypatch.setattr(engine, "_resolve_workspace_id", lambda consent_url: "ws-1")
|
|
monkeypatch.setattr(
|
|
RegistrationEngine,
|
|
"_select_workspace",
|
|
lambda self, workspace_id: "https://auth.example.test/continue",
|
|
)
|
|
monkeypatch.setattr(
|
|
RegistrationEngine,
|
|
"_follow_redirects",
|
|
lambda self, continue_url: "http://localhost:1455/auth/callback?code=code-1&state=state-1",
|
|
)
|
|
monkeypatch.setattr(
|
|
RegistrationEngine,
|
|
"_handle_oauth_callback",
|
|
lambda self, callback_url: {
|
|
"id_token": "id-token",
|
|
"access_token": "access-token",
|
|
"refresh_token": "refresh-token",
|
|
"account_id": "acct-1",
|
|
},
|
|
)
|
|
|
|
result = engine.run()
|
|
|
|
assert result.success is True
|
|
assert result.workspace_id == "ws-1"
|
|
assert result.auth_json["auth_mode"] == "chatgpt"
|
|
assert result.auth_json["OPENAI_API_KEY"] is None
|
|
assert result.auth_json["tokens"] == {
|
|
"id_token": "id-token",
|
|
"access_token": "access-token",
|
|
"refresh_token": "refresh-token",
|
|
"account_id": "acct-1",
|
|
}
|
|
assert result.auth_json["last_refresh"]
|
|
assert seen["anchor_before_password"] == 1_700_000_000.0
|
|
assert seen["anchor_before_wait"] == 1_700_000_000.0
|
|
assert seen["otp_wait_started_at"] == 1_700_000_000.0
|
|
|
|
|
|
def test_resolve_workspace_id_falls_back_to_cookie_path_when_consent_page_has_no_workspace(monkeypatch):
|
|
engine = _build_engine(monkeypatch)
|
|
consent_url = "https://auth.example.test/consent"
|
|
engine.oauth_start = SimpleNamespace(auth_url="https://auth.example.test/oauth/authorize")
|
|
engine.session = FakeSession(
|
|
FakeResponse(
|
|
status_code=200,
|
|
url=consent_url,
|
|
text="<html><body>consent</body></html>",
|
|
)
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
engine,
|
|
"_extract_workspace_id_from_response",
|
|
lambda response=None, html=None, url=None: None,
|
|
)
|
|
monkeypatch.setattr(RegistrationEngine, "_get_workspace_id", lambda self: "ws-cookie")
|
|
|
|
workspace_id = engine._resolve_workspace_id(consent_url)
|
|
|
|
assert workspace_id == "ws-cookie"
|
|
assert engine.session.calls == [{"url": consent_url, "kwargs": {"timeout": 20}}]
|