From 053f6633b139de6f81c8149dcdf22a83ca5fa025 Mon Sep 17 00:00:00 2001 From: Cooper Gamble Date: Tue, 19 May 2026 23:16:49 +0000 Subject: [PATCH] [codex-core] add revoked auth integration coverage [ci changed_files] --- codex-rs/core/src/client_tests.rs | 29 ++++++++++ codex-rs/core/src/thread_manager.rs | 2 +- codex-rs/core/tests/suite/client.rs | 89 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 7f6b2640bf..bdafbadaf4 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -716,6 +716,35 @@ async fn token_invalidated_error_type_401_clears_auth_and_requires_relogin() { ); } +#[tokio::test] +async fn token_revoked_error_code_401_clears_auth_and_requires_relogin() { + let (_codex_home, manager) = managed_chatgpt_auth_manager("revoked-access-token").await; + let mut recovery = Some(manager.unauthorized_recovery()); + + let err = handle_unauthorized( + TransportError::Http { + status: StatusCode::UNAUTHORIZED, + url: Some("https://chatgpt.com/backend-api/codex/responses".to_string()), + headers: None, + body: Some(r#"{"error":{"code":"token_revoked"}}"#.to_string()), + }, + &mut recovery, + &test_session_telemetry(), + ) + .await + .expect_err("revoked access tokens should force relogin"); + + let CodexErr::RefreshTokenFailed(failed) = err else { + panic!("expected revoked access token to force relogin, got {err:?}"); + }; + assert_eq!(failed.reason, RefreshTokenFailedReason::Revoked); + assert_eq!( + failed.message, + "Your ChatGPT session is no longer valid. Please sign in again." + ); + assert!(manager.auth_cached().is_none()); +} + #[tokio::test] async fn token_invalidated_401_retries_when_persisted_auth_changed() { let (codex_home, manager) = managed_chatgpt_auth_manager("revoked-access-token").await; diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index e5b4fcf6a2..cef6046d58 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -339,7 +339,7 @@ impl ThreadManager { state_db: Option, ) -> Self { set_thread_manager_test_mode_for_tests(/*enabled*/ true); - let auth_manager = AuthManager::from_auth_for_testing(auth); + let auth_manager = AuthManager::from_auth_for_testing_with_home(auth, codex_home.clone()); let installation_id = uuid::Uuid::new_v4().to_string(); let skills_codex_home = match AbsolutePathBuf::from_absolute_path_checked(&codex_home) { Ok(codex_home) => codex_home, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index f6731a0ea4..45cf7acdb1 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1066,6 +1066,95 @@ async fn chatgpt_auth_sends_correct_request() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn revoked_chatgpt_auth_user_turn_clears_auth_and_requests_relogin() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/codex/responses")) + .respond_with(ResponseTemplate::new(401).set_body_json(json!({ + "error": { + "code": "token_revoked", + "message": "revoked", + } + }))) + .expect(1) + .mount(&server) + .await; + + let codex_home = Arc::new(TempDir::new()?); + let _jwt = write_auth_json( + codex_home.as_ref(), + /*openai_api_key*/ None, + "pro", + "revoked-access-token", + Some("account_id"), + ); + let auth = CodexAuth::from_auth_storage( + codex_home.path(), + AuthCredentialsStoreMode::File, + /*chatgpt_base_url*/ None, + ) + .await? + .expect("managed ChatGPT auth should load"); + + let mut model_provider = built_in_model_providers(/* openai_base_url */ None)["openai"].clone(); + model_provider.base_url = Some(format!("{}/api/codex", server.uri())); + model_provider.supports_websockets = false; + let mut builder = test_codex() + .with_home(codex_home.clone()) + .with_auth(auth) + .with_config(move |config| { + config.model_provider = model_provider; + }); + let test = builder.build(&server).await?; + let codex = test.codex.clone(); + + codex + .submit(Op::UserInput { + environments: None, + items: vec![UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + }) + .await?; + + let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await; + assert!( + matches!( + error_event, + EventMsg::Error(ref err) + if err.message.contains( + "Your ChatGPT session is no longer valid. Please sign in again." + ) + ), + "expected invalidated-session relogin error; got {error_event:?}" + ); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + assert!( + !test.codex_home_path().join("auth.json").exists(), + "revoked managed ChatGPT auth should be removed" + ); + let response_attempts = server + .received_requests() + .await + .expect("mock server should capture requests") + .into_iter() + .filter(|request| request.url.path() == "/api/codex/responses") + .count(); + assert_eq!( + response_attempts, 1, + "revoked managed auth should fail without retrying /responses" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { skip_if_no_network!();