Files
codex/codex-rs/app-server/src/account_notifications_tests.rs
T
jif b2aa07db8a Add scoped leases for persisted account OAuth grants (#55546)
## What changed

- Expose `AccountOAuthGrantLease` to prevent this auth manager's token refreshes from rotating account credentials during an identity-provider exchange. Releasing the lease returns an `AccountOAuthGrant` that callers can recheck before accepting asynchronous results.
- Validate grants against cached login ownership and persisted credentials, requiring OAuth provenance and complete account identity. Reject stale grants and propagate storage failures, including unavailable keyrings in `Auto` mode.
- Track `login_generation` separately from account ownership and use it to invalidate enterprise MCP authentication. Losing login metadata invalidates the login while preserving same-account network policy; a new login to the same account advances both generations.

## Testing

Extend auth refresh coverage to verify refresh blocking and wakeup on lease release, invalidation after token rotation or persisted login replacement, metadata loss and restoration, and logout while a lease is held.

GitOrigin-RevId: f51b3cd526d7ee94d5dffa16681f8ca332245ce3
2026-10-20 11:48:54 +00:00

102 lines
3.6 KiB
Rust

//! Exercises auth changes while notification delivery waits for queue capacity.
use super::*;
use pretty_assertions::assert_eq;
use test_case::test_case;
#[test_case(0; "same_owner_refresh")]
#[test_case(1; "owner_change")]
#[test_case(2; "coalesced_owner_changes")]
#[tokio::test]
async fn queued_notifications_follow_auth_owner_changes(owner_generation: u64) {
for login in [false, true] {
let (tx, mut rx) = mpsc::channel(/*buffer*/ 1);
let outgoing = OutgoingMessageSender::new(
tx.clone(),
codex_analytics::AnalyticsEventsClient::disabled(),
);
let (changes, auth_changes) = watch::channel(AuthChangeState::default());
let updated = AccountUpdatedNotification {
auth_mode: None,
plan_type: None,
};
tx.send(OutgoingEnvelope::Broadcast {
message: timestamped_server_notification(ServerNotification::AccountUpdated(
updated.clone(),
)),
})
.await
.unwrap();
let payload = AccountLoginCompletedNotification {
login_id: Some("queued-login".into()),
success: true,
error: None,
onboarding_entrypoint: None,
};
let notification = if login {
AccountNotification::LoginCompleted(payload.clone())
} else {
AccountNotification::Updated(updated.clone())
};
let send = outgoing.send_account_notification(
Some(ConnectionId(7)),
&auth_changes,
/*owner_generation*/ 0,
notification,
);
tokio::pin!(send);
// Poll delivery with a full queue before changing authentication.
assert!(futures::poll!(&mut send).is_pending());
changes.send_replace(AuthChangeState {
generation: 2,
owner_generation,
source_generation: owner_generation,
login_generation: owner_generation,
});
rx.recv().await.unwrap();
send.await;
if login {
let OutgoingEnvelope::ToConnection {
connection_id,
message: OutgoingMessage::AppServerNotification(envelope),
..
} = rx.try_recv().unwrap()
else {
panic!("expected targeted login completion");
};
assert_eq!(connection_id, ConnectionId(7));
let ServerNotification::AccountLoginCompleted(actual) = envelope.notification else {
panic!("expected login completion");
};
assert_eq!(
actual,
if owner_generation == 0 {
payload
} else {
AccountLoginCompletedNotification {
success: false,
error: Some("account changed before sign-in completed".into()),
..payload
}
}
);
} else if owner_generation == 0 {
let OutgoingEnvelope::ToConnection {
connection_id,
message: OutgoingMessage::AppServerNotification(envelope),
..
} = rx.try_recv().unwrap()
else {
panic!("expected targeted account update");
};
assert_eq!(connection_id, ConnectionId(7));
let ServerNotification::AccountUpdated(actual) = envelope.notification else {
panic!("expected account update");
};
assert_eq!(actual, updated);
} else {
assert!(rx.try_recv().is_err());
}
}
}