diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 309e4d892f..b4894cddcf 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -687,7 +687,10 @@ impl Client { } } - if matches!(*client.status().borrow(), Status::ConnectionError) { + if matches!( + *client.status().borrow(), + Status::AuthenticationError | Status::ConnectionError + ) { client.set_status( Status::ReconnectionError { next_reconnection: Instant::now() + delay, @@ -856,28 +859,14 @@ impl Client { let old_credentials = self.state.read().credentials.clone(); if let Some(old_credentials) = old_credentials { - if self - .cloud_client - .validate_credentials( - old_credentials.user_id as u32, - &old_credentials.access_token, - ) - .await? - { + if self.validate_credentials(&old_credentials, cx).await? { credentials = Some(old_credentials); } } if credentials.is_none() && try_provider { if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await { - if self - .cloud_client - .validate_credentials( - stored_credentials.user_id as u32, - &stored_credentials.access_token, - ) - .await? - { + if self.validate_credentials(&stored_credentials, cx).await? { credentials = Some(stored_credentials); } else { self.credentials_provider @@ -926,6 +915,24 @@ impl Client { Ok(credentials) } + async fn validate_credentials( + self: &Arc, + credentials: &Credentials, + cx: &AsyncApp, + ) -> Result { + match self + .cloud_client + .validate_credentials(credentials.user_id as u32, &credentials.access_token) + .await + { + Ok(valid) => Ok(valid), + Err(err) => { + self.set_status(Status::AuthenticationError, cx); + Err(anyhow!("failed to validate credentials: {}", err)) + } + } + } + /// Performs a sign-in and also connects to Collab. /// /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls @@ -1733,6 +1740,46 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } + #[gpui::test(iterations = 10)] + async fn test_auth_failure_during_reconnection(cx: &mut TestAppContext) { + init_test(cx); + let http_client = FakeHttpClient::with_200_response(); + let client = + cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client.clone(), cx)); + let server = FakeServer::for_client(42, &client, cx).await; + let mut status = client.status(); + assert!(matches!( + status.next().await, + Some(Status::Connected { .. }) + )); + assert_eq!(server.auth_count(), 1); + + // Simulate an auth failure during reconnection. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(503) + .body("".into()) + .unwrap()) + }); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + + // Restore the ability to authenticate. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(200) + .body("".into()) + .unwrap()) + }); + cx.executor().advance_clock(Duration::from_secs(10)); + while !matches!(status.next().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting + } + #[gpui::test(iterations = 10)] async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx);