Compare commits

...

2 Commits

Author SHA1 Message Date
celia-oai
41a653fd81 fix tests 2026-03-20 16:59:57 -07:00
celia-oai
703ed17fb6 changes 2026-03-20 16:29:57 -07:00
3 changed files with 196 additions and 35 deletions

View File

@@ -883,6 +883,24 @@ mod tests {
account_id: Option<&str>,
access_token: &str,
refresh_token: &str,
) -> serde_json::Value {
chatgpt_auth_json_with_last_refresh(
plan_type,
chatgpt_user_id,
account_id,
access_token,
refresh_token,
"2025-01-01T00:00:00Z",
)
}
fn chatgpt_auth_json_with_last_refresh(
plan_type: &str,
chatgpt_user_id: Option<&str>,
account_id: Option<&str>,
access_token: &str,
refresh_token: &str,
last_refresh: &str,
) -> serde_json::Value {
chatgpt_auth_json_with_mode(
plan_type,
@@ -890,6 +908,7 @@ mod tests {
account_id,
access_token,
refresh_token,
last_refresh,
None,
)
}
@@ -900,6 +919,7 @@ mod tests {
account_id: Option<&str>,
access_token: &str,
refresh_token: &str,
last_refresh: &str,
auth_mode: Option<&str>,
) -> serde_json::Value {
let header = json!({ "alg": "none", "typ": "JWT" });
@@ -925,7 +945,7 @@ mod tests {
"refresh_token": refresh_token,
"account_id": account_id,
},
"last_refresh": "2025-01-01T00:00:00Z",
"last_refresh": last_refresh,
});
if let Some(auth_mode) = auth_mode {
auth_json["auth_mode"] = serde_json::Value::String(auth_mode.to_string());
@@ -1262,24 +1282,43 @@ enabled = false
#[tokio::test]
async fn fetch_cloud_requirements_recovers_after_unauthorized_reload() {
let auth = managed_auth_context(
"business",
Some("user-12345"),
Some("account-12345"),
"stale-access-token",
"test-refresh-token",
);
let auth_home = tempdir().expect("tempdir");
write_auth_json(
auth._home.path(),
chatgpt_auth_json(
auth_home.path(),
chatgpt_auth_json_with_last_refresh(
"business",
Some("user-12345"),
Some("account-12345"),
"stale-access-token",
"test-refresh-token",
// Keep auth "fresh" so the first request hits unauthorized recovery
// instead of AuthManager::auth() proactively reloading from disk.
"3025-01-01T00:00:00Z",
),
)
.expect("write initial auth");
let auth_manager = Arc::new(AuthManager::new(
auth_home.path().to_path_buf(),
false,
AuthCredentialsStoreMode::File,
));
write_auth_json(
auth_home.path(),
chatgpt_auth_json_with_last_refresh(
"business",
Some("user-12345"),
Some("account-12345"),
"fresh-access-token",
"test-refresh-token",
"3025-01-01T00:00:00Z",
),
)
.expect("write refreshed auth");
let auth = ManagedAuthContext {
_home: auth_home,
manager: auth_manager,
};
let fetcher = Arc::new(TokenFetcher {
expected_token: "fresh-access-token".to_string(),
@@ -1314,24 +1353,41 @@ enabled = false
#[tokio::test]
async fn fetch_cloud_requirements_recovers_after_unauthorized_reload_updates_cache_identity() {
let auth = managed_auth_context(
"business",
Some("user-12345"),
Some("account-12345"),
"stale-access-token",
"test-refresh-token",
);
let auth_home = tempdir().expect("tempdir");
write_auth_json(
auth._home.path(),
chatgpt_auth_json(
auth_home.path(),
chatgpt_auth_json_with_last_refresh(
"business",
Some("user-12345"),
Some("account-12345"),
"stale-access-token",
"test-refresh-token",
"3025-01-01T00:00:00Z",
),
)
.expect("write initial auth");
let auth_manager = Arc::new(AuthManager::new(
auth_home.path().to_path_buf(),
false,
AuthCredentialsStoreMode::File,
));
write_auth_json(
auth_home.path(),
chatgpt_auth_json_with_last_refresh(
"business",
Some("user-99999"),
Some("account-12345"),
"fresh-access-token",
"test-refresh-token",
"3025-01-01T00:00:00Z",
),
)
.expect("write refreshed auth");
let auth = ManagedAuthContext {
_home: auth_home,
manager: auth_manager,
};
let fetcher = Arc::new(TokenFetcher {
expected_token: "fresh-access-token".to_string(),
@@ -1432,6 +1488,7 @@ enabled = false
Some("account-12345"),
"test-access-token",
"test-refresh-token",
"2025-01-01T00:00:00Z",
Some("chatgptAuthTokens"),
),
)

View File

@@ -381,6 +381,116 @@ async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
let ctx = RefreshTokenTestContext::new(&server)?;
let stale_refresh = Utc::now() - Duration::days(9);
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens),
last_refresh: Some(stale_refresh),
};
ctx.write_auth(&initial_auth)?;
let fresh_refresh = Utc::now() - Duration::days(1);
let disk_tokens = build_tokens("disk-access-token", "disk-refresh-token");
let disk_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(disk_tokens.clone()),
last_refresh: Some(fresh_refresh),
};
save_auth(
ctx.codex_home.path(),
&disk_auth,
AuthCredentialsStoreMode::File,
)?;
let cached_auth = ctx
.auth_manager
.auth()
.await
.context("auth should reload from disk")?;
let cached = cached_auth
.get_token_data()
.context("token data should reload from disk")?;
assert_eq!(cached, disk_tokens);
let stored = ctx.load_auth()?;
assert_eq!(stored, disk_auth);
let requests = server.received_requests().await.unwrap_or_default();
assert!(requests.is_empty(), "expected no refresh token requests");
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": {
"code": "refresh_token_expired"
}
})))
.expect(0)
.mount(&server)
.await;
let ctx = RefreshTokenTestContext::new(&server)?;
let stale_refresh = Utc::now() - Duration::days(9);
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens),
last_refresh: Some(stale_refresh),
};
ctx.write_auth(&initial_auth)?;
let fresh_refresh = Utc::now() - Duration::days(1);
let disk_tokens = build_tokens("disk-access-token", "disk-refresh-token");
let disk_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(disk_tokens.clone()),
last_refresh: Some(fresh_refresh),
};
save_auth(
ctx.codex_home.path(),
&disk_auth,
AuthCredentialsStoreMode::File,
)?;
let cached_auth = ctx
.auth_manager
.auth()
.await
.context("auth should reload from disk")?;
let cached = cached_auth
.get_token_data()
.context("token data should reload from disk")?;
assert_eq!(cached, disk_tokens);
let stored = ctx.load_auth()?;
assert_eq!(stored, disk_auth);
server.verify().await;
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Result<()> {

View File

@@ -1090,10 +1090,13 @@ impl AuthManager {
}
/// Current cached auth (clone). May be `None` if not logged in or load failed.
/// Refreshes cached ChatGPT tokens if they are stale before returning.
/// For stale managed ChatGPT auth, first performs a guarded reload and then
/// refreshes only if the on-disk auth is unchanged.
pub async fn auth(&self) -> Option<CodexAuth> {
let auth = self.auth_cached()?;
if let Err(err) = self.refresh_if_stale(&auth).await {
if Self::is_stale_for_proactive_refresh(&auth)
&& let Err(err) = self.refresh_token().await
{
tracing::error!("Failed to refresh token: {}", err);
return Some(auth);
}
@@ -1320,30 +1323,21 @@ impl AuthManager {
self.auth_cached().as_ref().map(CodexAuth::auth_mode)
}
async fn refresh_if_stale(&self, auth: &CodexAuth) -> Result<bool, RefreshTokenError> {
fn is_stale_for_proactive_refresh(auth: &CodexAuth) -> bool {
let chatgpt_auth = match auth {
CodexAuth::Chatgpt(chatgpt_auth) => chatgpt_auth,
_ => return Ok(false),
_ => return false,
};
let auth_dot_json = match chatgpt_auth.current_auth_json() {
Some(auth_dot_json) => auth_dot_json,
None => return Ok(false),
};
let tokens = match auth_dot_json.tokens {
Some(tokens) => tokens,
None => return Ok(false),
None => return false,
};
let last_refresh = match auth_dot_json.last_refresh {
Some(last_refresh) => last_refresh,
None => return Ok(false),
None => return false,
};
if last_refresh >= Utc::now() - chrono::Duration::days(TOKEN_REFRESH_INTERVAL) {
return Ok(false);
}
self.refresh_and_persist_chatgpt_token(chatgpt_auth, tokens.refresh_token)
.await?;
Ok(true)
last_refresh < Utc::now() - chrono::Duration::days(TOKEN_REFRESH_INTERVAL)
}
async fn refresh_external_auth(