Port login server to rust (#2294)
Port the login server to rust. --------- Co-authored-by: pakrym-oai <pakrym@openai.com>
This commit is contained in:
192
codex-rs/login/tests/login_server_e2e.rs
Normal file
192
codex-rs/login/tests/login_server_e2e.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpListener;
|
||||
use std::thread;
|
||||
|
||||
use base64::Engine;
|
||||
use codex_login::LoginServerInfo;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::run_server_blocking_with_notify;
|
||||
use tempfile::tempdir;
|
||||
|
||||
// See spawn.rs for details
|
||||
pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";
|
||||
|
||||
fn start_mock_issuer() -> (SocketAddr, thread::JoinHandle<()>) {
|
||||
// Bind to a random available port
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let server = tiny_http::Server::from_listener(listener, None).unwrap();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
while let Ok(mut req) = server.recv() {
|
||||
let url = req.url().to_string();
|
||||
if url.starts_with("/oauth/token") {
|
||||
// Read body
|
||||
let mut body = String::new();
|
||||
let _ = req.as_reader().read_to_string(&mut body);
|
||||
// Build minimal JWT with plan=pro
|
||||
#[derive(serde::Serialize)]
|
||||
struct Header {
|
||||
alg: &'static str,
|
||||
typ: &'static str,
|
||||
}
|
||||
let header = Header {
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
};
|
||||
let payload = serde_json::json!({
|
||||
"email": "user@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_plan_type": "pro",
|
||||
"chatgpt_account_id": "acc-123"
|
||||
}
|
||||
});
|
||||
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
|
||||
let header_bytes = serde_json::to_vec(&header).unwrap();
|
||||
let payload_bytes = serde_json::to_vec(&payload).unwrap();
|
||||
let id_token = format!(
|
||||
"{}.{}.{}",
|
||||
b64(&header_bytes),
|
||||
b64(&payload_bytes),
|
||||
b64(b"sig")
|
||||
);
|
||||
|
||||
let tokens = serde_json::json!({
|
||||
"id_token": id_token,
|
||||
"access_token": "access-123",
|
||||
"refresh_token": "refresh-123",
|
||||
});
|
||||
let data = serde_json::to_vec(&tokens).unwrap();
|
||||
let mut resp = tiny_http::Response::from_data(data);
|
||||
resp.add_header(
|
||||
tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..])
|
||||
.unwrap_or_else(|_| panic!("header bytes")),
|
||||
);
|
||||
let _ = req.respond(resp);
|
||||
} else {
|
||||
let _ = req
|
||||
.respond(tiny_http::Response::from_string("not found").with_status_code(404));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(addr, handle)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_to_end_login_flow_persists_auth_json() {
|
||||
if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||||
println!(
|
||||
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (issuer_addr, issuer_handle) = start_mock_issuer();
|
||||
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let codex_home = tmp.path().to_path_buf();
|
||||
|
||||
let state = "test_state_123".to_string();
|
||||
|
||||
// Run server in background
|
||||
let server_home = codex_home.clone();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel::<LoginServerInfo>();
|
||||
let server_thread = thread::spawn(move || {
|
||||
let opts = ServerOptions {
|
||||
codex_home: &server_home,
|
||||
client_id: codex_login::CLIENT_ID,
|
||||
issuer: &issuer,
|
||||
port: 0,
|
||||
open_browser: false,
|
||||
force_state: Some(state),
|
||||
};
|
||||
run_server_blocking_with_notify(opts, Some(tx), None).unwrap();
|
||||
});
|
||||
|
||||
let server_info = rx.recv().unwrap();
|
||||
let login_port = server_info.actual_port;
|
||||
|
||||
// Simulate browser callback, and follow redirect to /success
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=test_state_123");
|
||||
let resp = client.get(&url).send().unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
// Wait for server shutdown
|
||||
server_thread
|
||||
.join()
|
||||
.unwrap_or_else(|_| panic!("server thread panicked"));
|
||||
|
||||
// Validate auth.json
|
||||
let auth_path = codex_home.join("auth.json");
|
||||
let data = std::fs::read_to_string(&auth_path).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_str(&data).unwrap();
|
||||
assert!(
|
||||
!json["OPENAI_API_KEY"].is_null(),
|
||||
"OPENAI_API_KEY should be set"
|
||||
);
|
||||
assert_eq!(json["tokens"]["access_token"], "access-123");
|
||||
assert_eq!(json["tokens"]["refresh_token"], "refresh-123");
|
||||
assert_eq!(json["tokens"]["account_id"], "acc-123");
|
||||
|
||||
// Stop mock issuer
|
||||
drop(issuer_handle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_missing_codex_home_dir() {
|
||||
if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||||
println!(
|
||||
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (issuer_addr, _issuer_handle) = start_mock_issuer();
|
||||
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let codex_home = tmp.path().join("missing-subdir"); // does not exist
|
||||
|
||||
let state = "state2".to_string();
|
||||
|
||||
// Run server in background
|
||||
let server_home = codex_home.clone();
|
||||
let (tx, rx) = std::sync::mpsc::channel::<LoginServerInfo>();
|
||||
let server_thread = thread::spawn(move || {
|
||||
let opts = ServerOptions {
|
||||
codex_home: &server_home,
|
||||
client_id: codex_login::CLIENT_ID,
|
||||
issuer: &issuer,
|
||||
port: 0,
|
||||
open_browser: false,
|
||||
force_state: Some(state),
|
||||
};
|
||||
run_server_blocking_with_notify(opts, Some(tx), None).unwrap()
|
||||
});
|
||||
|
||||
let server_info = rx.recv().unwrap();
|
||||
let login_port = server_info.actual_port;
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=state2");
|
||||
let resp = client.get(&url).send().unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
|
||||
server_thread
|
||||
.join()
|
||||
.unwrap_or_else(|_| panic!("server thread panicked"));
|
||||
|
||||
let auth_path = codex_home.join("auth.json");
|
||||
assert!(
|
||||
auth_path.exists(),
|
||||
"auth.json should be created even if parent dir was missing"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user