mirror of
https://github.com/openai/codex.git
synced 2026-08-28 13:58:49 +00:00
## Why Keep the TestAppServer builder API change reviewable by moving the repository-wide caller migration into a mechanical follow-up. ## What - replace TestAppServer constructor callsites with equivalent builder chains - preserve each caller's automatic-environment, args, program, env, managed-config, plugin-startup, and JSON-logging behavior - make no TestAppServer implementation changes ## Validation - cargo check -p codex-app-server --tests ## Cleanup stack 1. [#31425 test: add TestAppServer builder](https://github.com/openai/codex/pull/31425) 2. [#31451 test: migrate TestAppServer callers to builder](https://github.com/openai/codex/pull/31451) 3. [#31452 test: remove TestAppServer constructors](https://github.com/openai/codex/pull/31452)
649 lines
20 KiB
Rust
649 lines
20 KiB
Rust
use anyhow::Result;
|
|
use app_test_support::ChatGptAuthFixture;
|
|
use app_test_support::TestAppServer;
|
|
use app_test_support::to_response;
|
|
use app_test_support::write_chatgpt_auth;
|
|
use chrono::Duration;
|
|
use chrono::Utc;
|
|
use codex_app_server_protocol::Account;
|
|
use codex_app_server_protocol::AuthMode;
|
|
use codex_app_server_protocol::GetAccountParams;
|
|
use codex_app_server_protocol::GetAccountResponse;
|
|
use codex_app_server_protocol::GetAuthStatusParams;
|
|
use codex_app_server_protocol::GetAuthStatusResponse;
|
|
use codex_app_server_protocol::JSONRPCError;
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
use codex_app_server_protocol::LoginAccountResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
|
use codex_protocol::account::PlanType as AccountPlanType;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
use tokio::time::timeout;
|
|
use wiremock::Mock;
|
|
use wiremock::MockServer;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::header;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
|
|
// Bazel CI can spend tens of seconds starting app-server subprocesses or
|
|
// processing auth RPCs under load.
|
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
|
|
|
fn create_config_toml_custom_provider(
|
|
codex_home: &Path,
|
|
requires_openai_auth: bool,
|
|
) -> std::io::Result<()> {
|
|
let config_toml = codex_home.join("config.toml");
|
|
let requires_line = if requires_openai_auth {
|
|
"requires_openai_auth = true\n"
|
|
} else {
|
|
""
|
|
};
|
|
let contents = format!(
|
|
r#"
|
|
model = "mock-model"
|
|
approval_policy = "never"
|
|
sandbox_mode = "danger-full-access"
|
|
|
|
model_provider = "mock_provider"
|
|
|
|
[features]
|
|
shell_snapshot = false
|
|
|
|
[model_providers.mock_provider]
|
|
name = "Mock provider for test"
|
|
base_url = "http://127.0.0.1:0/v1"
|
|
wire_api = "responses"
|
|
request_max_retries = 0
|
|
stream_max_retries = 0
|
|
{requires_line}
|
|
"#
|
|
);
|
|
std::fs::write(config_toml, contents)
|
|
}
|
|
|
|
fn create_config_toml(codex_home: &Path) -> std::io::Result<()> {
|
|
let config_toml = codex_home.join("config.toml");
|
|
std::fs::write(
|
|
config_toml,
|
|
r#"
|
|
model = "mock-model"
|
|
approval_policy = "never"
|
|
sandbox_mode = "danger-full-access"
|
|
|
|
[features]
|
|
shell_snapshot = false
|
|
"#,
|
|
)
|
|
}
|
|
|
|
fn create_config_toml_forced_login(codex_home: &Path, forced_method: &str) -> std::io::Result<()> {
|
|
let config_toml = codex_home.join("config.toml");
|
|
let contents = format!(
|
|
r#"
|
|
model = "mock-model"
|
|
approval_policy = "never"
|
|
sandbox_mode = "danger-full-access"
|
|
forced_login_method = "{forced_method}"
|
|
|
|
[features]
|
|
shell_snapshot = false
|
|
"#
|
|
);
|
|
std::fs::write(config_toml, contents)
|
|
}
|
|
|
|
async fn login_with_api_key_via_request(mcp: &mut TestAppServer, api_key: &str) -> Result<()> {
|
|
let request_id = mcp.send_login_account_api_key_request(api_key).await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let response: LoginAccountResponse = to_response(resp)?;
|
|
assert_eq!(response, LoginAccountResponse::ApiKey {});
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_no_auth() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(false),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(status.auth_method, None, "expected no auth method");
|
|
assert_eq!(status.auth_token, None, "expected no token");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_with_api_key() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(false),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(status.auth_method, Some(AuthMode::ApiKey));
|
|
assert_eq!(status.auth_token, Some("sk-test-key".to_string()));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn personal_access_token_without_email_supports_auth_status_and_account_read() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/v1/user-auth-credential/whoami"))
|
|
.and(header("Authorization", "Bearer at-test-token"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
"email": null,
|
|
"chatgpt_user_id": "user-123",
|
|
"chatgpt_account_id": "account-123",
|
|
"chatgpt_plan_type": "pro",
|
|
"chatgpt_account_is_fedramp": false,
|
|
})))
|
|
.expect(1..)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let authapi_base_url = server.uri();
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.with_env_overrides(&[
|
|
("OPENAI_API_KEY", None),
|
|
("CODEX_ACCESS_TOKEN", Some("at-test-token")),
|
|
("CODEX_AUTHAPI_BASE_URL", Some(authapi_base_url.as_str())),
|
|
])
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(false),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(
|
|
status,
|
|
GetAuthStatusResponse {
|
|
auth_method: Some(AuthMode::PersonalAccessToken),
|
|
auth_token: None,
|
|
requires_openai_auth: Some(true),
|
|
}
|
|
);
|
|
|
|
let request_id = mcp
|
|
.send_get_account_request(GetAccountParams {
|
|
refresh_token: false,
|
|
})
|
|
.await?;
|
|
let response: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
assert_eq!(
|
|
response
|
|
.result
|
|
.get("account")
|
|
.and_then(|account| account.get("email")),
|
|
Some(&serde_json::Value::Null),
|
|
);
|
|
assert_eq!(
|
|
to_response::<GetAccountResponse>(response)?,
|
|
GetAccountResponse {
|
|
account: Some(Account::Chatgpt {
|
|
email: None,
|
|
plan_type: AccountPlanType::Pro,
|
|
}),
|
|
requires_openai_auth: true,
|
|
}
|
|
);
|
|
|
|
server.verify().await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml_custom_provider(codex_home.path(), /*requires_openai_auth*/ false)?;
|
|
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(false),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(status.auth_method, None, "expected no auth method");
|
|
assert_eq!(status.auth_token, None, "expected no token");
|
|
assert_eq!(
|
|
status.requires_openai_auth,
|
|
Some(false),
|
|
"requires_openai_auth should be false",
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_with_api_key_no_include_token() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
|
|
|
// Build params via struct so None field is omitted in wire JSON.
|
|
let params = GetAuthStatusParams {
|
|
include_token: None,
|
|
refresh_token: Some(false),
|
|
};
|
|
let request_id = mcp.send_get_auth_status_request(params).await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(status.auth_method, Some(AuthMode::ApiKey));
|
|
assert!(status.auth_token.is_none(), "token must be omitted");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_with_api_key_refresh_requested() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(true),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(
|
|
status,
|
|
GetAuthStatusResponse {
|
|
auth_method: Some(AuthMode::ApiKey),
|
|
auth_token: Some("sk-test-key".to_string()),
|
|
requires_openai_auth: Some(true),
|
|
}
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_omits_token_after_permanent_refresh_failure() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
write_chatgpt_auth(
|
|
codex_home.path(),
|
|
ChatGptAuthFixture::new("stale-access-token")
|
|
.refresh_token("stale-refresh-token")
|
|
.account_id("acct_123")
|
|
.email("user@example.com")
|
|
.plan_type("pro"),
|
|
AuthCredentialsStoreMode::File,
|
|
)?;
|
|
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/oauth/token"))
|
|
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
|
|
"error": {
|
|
"code": "refresh_token_reused"
|
|
}
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let refresh_url = format!("{}/oauth/token", server.uri());
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.with_env_overrides(&[
|
|
("OPENAI_API_KEY", None),
|
|
(
|
|
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
|
Some(refresh_url.as_str()),
|
|
),
|
|
])
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(true),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(
|
|
status,
|
|
GetAuthStatusResponse {
|
|
auth_method: Some(AuthMode::Chatgpt),
|
|
auth_token: None,
|
|
requires_openai_auth: Some(true),
|
|
}
|
|
);
|
|
|
|
let second_request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(true),
|
|
})
|
|
.await?;
|
|
|
|
let second_resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(second_request_id)),
|
|
)
|
|
.await??;
|
|
let second_status: GetAuthStatusResponse = to_response(second_resp)?;
|
|
assert_eq!(second_status, status);
|
|
|
|
server.verify().await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_omits_token_after_proactive_refresh_failure() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
write_chatgpt_auth(
|
|
codex_home.path(),
|
|
ChatGptAuthFixture::new("stale-access-token")
|
|
.refresh_token("stale-refresh-token")
|
|
.account_id("acct_123")
|
|
.email("user@example.com")
|
|
.plan_type("pro")
|
|
.last_refresh(Some(Utc::now() - Duration::days(9))),
|
|
AuthCredentialsStoreMode::File,
|
|
)?;
|
|
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/oauth/token"))
|
|
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
|
|
"error": {
|
|
"code": "refresh_token_reused"
|
|
}
|
|
})))
|
|
.expect(2)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let refresh_url = format!("{}/oauth/token", server.uri());
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.with_env_overrides(&[
|
|
("OPENAI_API_KEY", None),
|
|
(
|
|
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
|
Some(refresh_url.as_str()),
|
|
),
|
|
])
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(false),
|
|
})
|
|
.await?;
|
|
|
|
let resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
let status: GetAuthStatusResponse = to_response(resp)?;
|
|
assert_eq!(
|
|
status,
|
|
GetAuthStatusResponse {
|
|
auth_method: Some(AuthMode::Chatgpt),
|
|
auth_token: None,
|
|
requires_openai_auth: Some(true),
|
|
}
|
|
);
|
|
|
|
server.verify().await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml(codex_home.path())?;
|
|
write_chatgpt_auth(
|
|
codex_home.path(),
|
|
ChatGptAuthFixture::new("stale-access-token")
|
|
.refresh_token("stale-refresh-token")
|
|
.account_id("acct_123")
|
|
.email("user@example.com")
|
|
.plan_type("pro")
|
|
.last_refresh(Some(Utc::now() - Duration::days(9))),
|
|
AuthCredentialsStoreMode::File,
|
|
)?;
|
|
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/oauth/token"))
|
|
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
|
|
"error": {
|
|
"code": "refresh_token_reused"
|
|
}
|
|
})))
|
|
.expect(2)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let refresh_url = format!("{}/oauth/token", server.uri());
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.with_env_overrides(&[
|
|
("OPENAI_API_KEY", None),
|
|
(
|
|
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
|
Some(refresh_url.as_str()),
|
|
),
|
|
])
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let failed_request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(true),
|
|
})
|
|
.await?;
|
|
|
|
let failed_resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(failed_request_id)),
|
|
)
|
|
.await??;
|
|
let failed_status: GetAuthStatusResponse = to_response(failed_resp)?;
|
|
assert_eq!(
|
|
failed_status,
|
|
GetAuthStatusResponse {
|
|
auth_method: Some(AuthMode::Chatgpt),
|
|
auth_token: None,
|
|
requires_openai_auth: Some(true),
|
|
}
|
|
);
|
|
|
|
write_chatgpt_auth(
|
|
codex_home.path(),
|
|
ChatGptAuthFixture::new("recovered-access-token")
|
|
.refresh_token("recovered-refresh-token")
|
|
.account_id("acct_123")
|
|
.email("user@example.com")
|
|
.plan_type("pro")
|
|
.last_refresh(Some(Utc::now())),
|
|
AuthCredentialsStoreMode::File,
|
|
)?;
|
|
|
|
let recovered_request_id = mcp
|
|
.send_get_auth_status_request(GetAuthStatusParams {
|
|
include_token: Some(true),
|
|
refresh_token: Some(false),
|
|
})
|
|
.await?;
|
|
|
|
let recovered_resp: JSONRPCResponse = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_response_message(RequestId::Integer(recovered_request_id)),
|
|
)
|
|
.await??;
|
|
let recovered_status: GetAuthStatusResponse = to_response(recovered_resp)?;
|
|
assert_eq!(
|
|
recovered_status,
|
|
GetAuthStatusResponse {
|
|
auth_method: Some(AuthMode::Chatgpt),
|
|
auth_token: Some("recovered-access-token".to_string()),
|
|
requires_openai_auth: Some(true),
|
|
}
|
|
);
|
|
|
|
server.verify().await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
create_config_toml_forced_login(codex_home.path(), "chatgpt")?;
|
|
|
|
let mut mcp = TestAppServer::builder()
|
|
.with_codex_home(codex_home.path())
|
|
.without_auto_env()
|
|
.build()
|
|
.await?;
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
|
|
|
let request_id = mcp
|
|
.send_login_account_api_key_request("sk-test-key")
|
|
.await?;
|
|
|
|
let err: JSONRPCError = timeout(
|
|
DEFAULT_READ_TIMEOUT,
|
|
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
|
)
|
|
.await??;
|
|
|
|
assert_eq!(
|
|
err.error.message,
|
|
"API key login is disabled. Use ChatGPT login instead."
|
|
);
|
|
Ok(())
|
|
}
|