fix(auth): reconcile expired desktop sessions
This commit is contained in:
@@ -380,7 +380,7 @@ fn run_sync_upload(
|
||||
match outcome {
|
||||
Ok(ely_browser_core::SyncOutcome::SignedOut) => {
|
||||
tracing::info!(target: "ely::sync", "no bearer token on disk; sync skipped");
|
||||
let _ = inbox.send(SyncStateUpdate::SignedOut { profile_id });
|
||||
let _ = inbox.send(SyncStateUpdate::AuthenticationExpired { profile_id });
|
||||
}
|
||||
Ok(ely_browser_core::SyncOutcome::AwaitingDeviceApproval { device_id }) => {
|
||||
tracing::info!(
|
||||
|
||||
@@ -300,7 +300,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credential_failures_do_not_finish_an_upload() {
|
||||
fn credential_failures_use_the_unavailable_update() {
|
||||
let profile_id = ProfileId::new();
|
||||
let update = device_failure_update(
|
||||
profile_id.clone(),
|
||||
@@ -311,12 +311,27 @@ mod tests {
|
||||
update,
|
||||
SyncStateUpdate::CredentialUnavailable {
|
||||
profile_id: owner,
|
||||
finishes_upload: false,
|
||||
..
|
||||
} if owner == profile_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_sessions_expire_profile_authentication() {
|
||||
let profile_id = ProfileId::new();
|
||||
for error in [
|
||||
ely_sync_client::SyncClientError::SessionEnded,
|
||||
ely_sync_client::SyncClientError::SessionChanged,
|
||||
] {
|
||||
let update = device_failure_update(profile_id.clone(), error);
|
||||
|
||||
assert!(matches!(
|
||||
update,
|
||||
SyncStateUpdate::AuthenticationExpired { profile_id: owner } if owner == profile_id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_failures_do_not_finish_an_upload() {
|
||||
let profile_id = ProfileId::new();
|
||||
|
||||
@@ -23,6 +23,24 @@ impl SyncStateMessage {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn early_reconciliation_priority(update: &SyncStateUpdate) -> Option<u8> {
|
||||
match update {
|
||||
SyncStateUpdate::AuthenticationExpired { .. } => Some(0),
|
||||
SyncStateUpdate::CredentialUnavailable { .. } => Some(1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn early_reconciliation_messages(
|
||||
messages: &[SyncStateMessage],
|
||||
) -> impl Iterator<Item = &SyncStateMessage> {
|
||||
[0_u8, 1].into_iter().flat_map(move |priority| {
|
||||
messages
|
||||
.iter()
|
||||
.filter(move |message| early_reconciliation_priority(&message.update) == Some(priority))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct SyncStateSender {
|
||||
generation: SyncWorkGeneration,
|
||||
@@ -46,12 +64,20 @@ impl ElyShell {
|
||||
pub(super) fn invalidate_sync_work(&mut self) {
|
||||
self.release_auth_flow_barrier();
|
||||
self.sync_generation.advance();
|
||||
self.reset_sync_work_state();
|
||||
}
|
||||
|
||||
pub(super) fn reset_sync_work_state(&mut self) {
|
||||
self.sync_upload_scheduled = false;
|
||||
self.sync_upload_in_flight = false;
|
||||
self.sync_retry_at = None;
|
||||
self.clear_pending_cloud_sync_upload();
|
||||
self.sync_devices.reset();
|
||||
}
|
||||
|
||||
pub(super) fn expire_authenticated_work(&mut self) {
|
||||
self.invalidate_sync_work();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -65,7 +91,7 @@ mod tests {
|
||||
let generation = SyncWorkGeneration(7);
|
||||
let sender = SyncStateSender { generation, sender };
|
||||
|
||||
sender.send(SyncStateUpdate::SignedOut { profile_id: ProfileId::new() })?;
|
||||
sender.send(SyncStateUpdate::AuthenticationExpired { profile_id: ProfileId::new() })?;
|
||||
|
||||
assert_eq!(receiver.recv()?.generation, generation);
|
||||
Ok(())
|
||||
@@ -80,9 +106,63 @@ mod tests {
|
||||
current.advance();
|
||||
current.advance();
|
||||
|
||||
old_sender.send(SyncStateUpdate::SignedOut { profile_id: ProfileId::new() })?;
|
||||
old_sender.send(SyncStateUpdate::AuthenticationExpired { profile_id: ProfileId::new() })?;
|
||||
|
||||
assert!(!receiver.recv()?.belongs_to(current));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn early_reconciliation_order_is_queue_independent() {
|
||||
let profile_id = ProfileId::new();
|
||||
let generation = SyncWorkGeneration::default();
|
||||
let expired = SyncStateMessage {
|
||||
generation,
|
||||
update: SyncStateUpdate::AuthenticationExpired { profile_id: profile_id.clone() },
|
||||
};
|
||||
let unavailable = SyncStateMessage {
|
||||
generation,
|
||||
update: SyncStateUpdate::CredentialUnavailable {
|
||||
profile_id,
|
||||
message: "locked".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
for messages in
|
||||
[vec![expired.clone(), unavailable.clone()], vec![unavailable.clone(), expired.clone()]]
|
||||
{
|
||||
let ordered = early_reconciliation_messages(&messages)
|
||||
.map(|message| early_reconciliation_priority(&message.update))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(ordered, [Some(0), Some(1)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn authentication_expiry_resets_scoped_work(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(gpui_component::init);
|
||||
let (shell, cx) = cx.add_window_view(ElyShell::new);
|
||||
|
||||
cx.update(|_, app_cx| {
|
||||
shell.update(app_cx, |shell, _| {
|
||||
let generation = shell.sync_generation;
|
||||
shell.sync_upload_scheduled = true;
|
||||
shell.sync_upload_in_flight = true;
|
||||
shell.sync_upload_pending = true;
|
||||
shell.sync_retry_at = Some(std::time::Instant::now());
|
||||
shell
|
||||
.sync_devices
|
||||
.set_error(ProfileId::new(), "expired device session".to_string());
|
||||
|
||||
shell.expire_authenticated_work();
|
||||
|
||||
assert_ne!(shell.sync_generation, generation);
|
||||
assert!(!shell.sync_upload_scheduled);
|
||||
assert!(!shell.sync_upload_in_flight);
|
||||
assert!(!shell.sync_upload_pending);
|
||||
assert!(shell.sync_retry_at.is_none());
|
||||
assert!(shell.sync_devices.error().is_none());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ use ely_domain::{ProfileId, ProfileKind, SyncConnectionState};
|
||||
use ely_sync_client::{AuthenticatedSnapshotHead, BearerToken, BearerTokenStore, DeviceRecord};
|
||||
use gpui::{Context, Timer};
|
||||
|
||||
use super::{ElyShell, ShellState, auth};
|
||||
use super::{
|
||||
ElyShell, ShellState, auth,
|
||||
sync_inbox::{early_reconciliation_messages, early_reconciliation_priority},
|
||||
};
|
||||
|
||||
mod failure;
|
||||
mod legacy_sync_migration;
|
||||
mod session_expiry;
|
||||
mod sign_out;
|
||||
|
||||
pub(super) use failure::{device_failure_update, sync_failure_update};
|
||||
@@ -26,17 +30,16 @@ pub(crate) struct PendingMergeUpload {
|
||||
/// Messages the off-thread sync workers push back to the shell so
|
||||
/// `SyncConnectionState` on `BrowserCore` and the in-flight auth
|
||||
/// form reflect live state without the UI thread ever touching the
|
||||
/// network. `SignedIn` is the initial-probe state set synchronously
|
||||
/// on shell startup and does not flow through this channel.
|
||||
/// network. Startup seeds `SignedIn` outside this channel.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum SyncStateUpdate {
|
||||
SignedOut { profile_id: ProfileId },
|
||||
AuthenticationExpired { profile_id: ProfileId },
|
||||
AwaitingDeviceApproval { profile_id: ProfileId, finishes_upload: bool },
|
||||
RemoteSnapshot { profile_id: ProfileId, bytes: Vec<u8>, merge: PendingMergeUpload },
|
||||
SyncReady { profile_id: ProfileId, last_synced_at_secs: u64 },
|
||||
SyncBusy { profile_id: ProfileId },
|
||||
SyncError { profile_id: ProfileId, message: String },
|
||||
CredentialUnavailable { profile_id: ProfileId, message: String, finishes_upload: bool },
|
||||
CredentialUnavailable { profile_id: ProfileId, message: String },
|
||||
DevicesLoaded { profile_id: ProfileId, devices: Vec<DeviceRecord>, current_code: String },
|
||||
DevicesError { profile_id: ProfileId, message: String },
|
||||
SignOutSucceeded { profile_id: ProfileId },
|
||||
@@ -172,7 +175,48 @@ impl ElyShell {
|
||||
let mut trigger_merged_upload = None;
|
||||
let mut upload_finished = false;
|
||||
let mut devices_changed = false;
|
||||
while let Ok(message) = self.sync_inbox_rx.try_recv() {
|
||||
let messages = self.sync_inbox_rx.try_iter().collect::<Vec<_>>();
|
||||
for state_message in early_reconciliation_messages(&messages) {
|
||||
match &state_message.update {
|
||||
SyncStateUpdate::AuthenticationExpired { profile_id } => {
|
||||
let current = state_message.belongs_to(self.sync_generation);
|
||||
match self.reconcile_authentication_expiry(profile_id, current) {
|
||||
session_expiry::AuthenticationExpiryResolution::Ignored => {}
|
||||
session_expiry::AuthenticationExpiryResolution::ReplacementCredential => {
|
||||
latest_connection = Some(SyncConnectionState::SignedIn);
|
||||
trigger_initial_sync = true;
|
||||
devices_changed = true;
|
||||
auth_changed = true;
|
||||
}
|
||||
session_expiry::AuthenticationExpiryResolution::Connection(connection) => {
|
||||
latest_connection = Some(connection);
|
||||
trigger_initial_sync = false;
|
||||
trigger_merged_upload = None;
|
||||
devices_changed = true;
|
||||
auth_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
SyncStateUpdate::CredentialUnavailable { profile_id, message }
|
||||
if state_message.belongs_to(self.sync_generation)
|
||||
&& active_profile_id(&self.state).as_ref() == Some(profile_id) =>
|
||||
{
|
||||
self.reconcile_credential_unavailable(profile_id);
|
||||
latest_connection = Some(SyncConnectionState::CredentialUnavailable {
|
||||
message: message.clone(),
|
||||
});
|
||||
trigger_initial_sync = false;
|
||||
trigger_merged_upload = None;
|
||||
devices_changed = true;
|
||||
auth_changed = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for message in messages {
|
||||
if early_reconciliation_priority(&message.update).is_some() {
|
||||
continue;
|
||||
}
|
||||
if let Some((profile_id, connection)) =
|
||||
self.reconcile_sign_out_update(message.generation, &message.update)
|
||||
{
|
||||
@@ -191,12 +235,8 @@ impl ElyShell {
|
||||
}
|
||||
let update = message.update;
|
||||
match update {
|
||||
SyncStateUpdate::SignedOut { profile_id } => {
|
||||
upload_finished = true;
|
||||
if active_profile_id(&self.state).as_ref() == Some(&profile_id) {
|
||||
latest_connection = Some(SyncConnectionState::SignedOut);
|
||||
}
|
||||
}
|
||||
SyncStateUpdate::AuthenticationExpired { .. }
|
||||
| SyncStateUpdate::CredentialUnavailable { .. } => {}
|
||||
SyncStateUpdate::AwaitingDeviceApproval { profile_id, finishes_upload } => {
|
||||
upload_finished |= finishes_upload;
|
||||
if active_profile_id(&self.state).as_ref() == Some(&profile_id) {
|
||||
@@ -258,20 +298,6 @@ impl ElyShell {
|
||||
latest_connection = Some(SyncConnectionState::SyncError { message });
|
||||
}
|
||||
}
|
||||
SyncStateUpdate::CredentialUnavailable { profile_id, message, finishes_upload } => {
|
||||
upload_finished |= finishes_upload;
|
||||
if active_profile_id(&self.state).as_ref() == Some(&profile_id) {
|
||||
latest_connection =
|
||||
Some(SyncConnectionState::CredentialUnavailable { message });
|
||||
self.sync_devices.reset();
|
||||
self.sync_upload_scheduled = false;
|
||||
self.sync_retry_at = None;
|
||||
self.clear_pending_cloud_sync_upload();
|
||||
trigger_initial_sync = false;
|
||||
trigger_merged_upload = None;
|
||||
devices_changed = true;
|
||||
}
|
||||
}
|
||||
SyncStateUpdate::DevicesLoaded { profile_id, devices, current_code } => {
|
||||
if active_profile_id(&self.state).as_ref() == Some(&profile_id) {
|
||||
self.sync_devices.set_ready(profile_id, devices, current_code);
|
||||
|
||||
@@ -8,10 +8,14 @@ pub(in crate::shell) fn sync_failure_update(
|
||||
) -> SyncStateUpdate {
|
||||
let message = error.to_string();
|
||||
match error {
|
||||
ely_sync_client::SyncClientError::SessionEnded
|
||||
| ely_sync_client::SyncClientError::SessionChanged => {
|
||||
SyncStateUpdate::AuthenticationExpired { profile_id }
|
||||
}
|
||||
ely_sync_client::SyncClientError::BearerCredentialStorage(_)
|
||||
| ely_sync_client::SyncClientError::AccountKeyStorage(_)
|
||||
| ely_sync_client::SyncClientError::DeviceKeyStorage(_) => {
|
||||
SyncStateUpdate::CredentialUnavailable { profile_id, message, finishes_upload: true }
|
||||
SyncStateUpdate::CredentialUnavailable { profile_id, message }
|
||||
}
|
||||
ely_sync_client::SyncClientError::DeviceApprovalStatus { .. } => {
|
||||
SyncStateUpdate::AwaitingDeviceApproval { profile_id, finishes_upload: true }
|
||||
@@ -28,9 +32,6 @@ pub(in crate::shell) fn device_failure_update(
|
||||
error: ely_sync_client::SyncClientError,
|
||||
) -> SyncStateUpdate {
|
||||
match sync_failure_update(profile_id, error) {
|
||||
SyncStateUpdate::CredentialUnavailable { profile_id, message, .. } => {
|
||||
SyncStateUpdate::CredentialUnavailable { profile_id, message, finishes_upload: false }
|
||||
}
|
||||
SyncStateUpdate::AwaitingDeviceApproval { profile_id, .. } => {
|
||||
SyncStateUpdate::AwaitingDeviceApproval { profile_id, finishes_upload: false }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
use ely_domain::{ProfileId, SyncConnectionState};
|
||||
|
||||
use super::super::{ElyShell, ShellState, auth};
|
||||
use crate::services::servo_profile_data::{default_profile_data_root, sync_profile_data_dir};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CredentialState {
|
||||
Present,
|
||||
Missing,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
pub(super) enum AuthenticationExpiryResolution {
|
||||
Ignored,
|
||||
ReplacementCredential,
|
||||
Connection(SyncConnectionState),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum ExpiryDecision {
|
||||
Ignore,
|
||||
RefreshReplacement { preserve_generation: bool },
|
||||
Reconcile { connection: SyncConnectionState, preserve_generation: bool, finish_sign_out: bool },
|
||||
}
|
||||
|
||||
impl ElyShell {
|
||||
pub(super) fn reconcile_authentication_expiry(
|
||||
&mut self,
|
||||
profile_id: &ProfileId,
|
||||
is_current_generation: bool,
|
||||
) -> AuthenticationExpiryResolution {
|
||||
let ShellState::Ready(core) = &self.state else {
|
||||
return AuthenticationExpiryResolution::Ignored;
|
||||
};
|
||||
let active_profile_id = match core.snapshot() {
|
||||
Ok(snapshot) => snapshot.active_profile_id,
|
||||
Err(_) => return AuthenticationExpiryResolution::Ignored,
|
||||
};
|
||||
let active_matches = &active_profile_id == profile_id;
|
||||
if !active_matches {
|
||||
return AuthenticationExpiryResolution::Ignored;
|
||||
}
|
||||
|
||||
let decision = expiry_decision(
|
||||
active_matches,
|
||||
is_current_generation,
|
||||
self.sign_out_phases.contains_key(profile_id),
|
||||
self.credential_state(profile_id),
|
||||
preserves_auth_attempt(&self.auth_flow_phase, profile_id),
|
||||
);
|
||||
match decision {
|
||||
ExpiryDecision::Ignore => AuthenticationExpiryResolution::Ignored,
|
||||
ExpiryDecision::RefreshReplacement { preserve_generation } => {
|
||||
if preserve_generation {
|
||||
self.reset_sync_work_state();
|
||||
} else {
|
||||
self.invalidate_sync_work();
|
||||
}
|
||||
AuthenticationExpiryResolution::ReplacementCredential
|
||||
}
|
||||
ExpiryDecision::Reconcile { connection, preserve_generation, finish_sign_out } => {
|
||||
if finish_sign_out {
|
||||
self.sign_out_phases.remove(profile_id);
|
||||
}
|
||||
self.reset_after_credential_loss(preserve_generation);
|
||||
AuthenticationExpiryResolution::Connection(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reconcile_credential_unavailable(&mut self, profile_id: &ProfileId) {
|
||||
let preserve_generation = preserves_auth_attempt(&self.auth_flow_phase, profile_id);
|
||||
self.reset_after_credential_loss(preserve_generation);
|
||||
}
|
||||
|
||||
fn reset_after_credential_loss(&mut self, preserve_generation: bool) {
|
||||
if preserve_generation {
|
||||
self.reset_sync_work_state();
|
||||
} else {
|
||||
self.expire_authenticated_work();
|
||||
self.auth_flow_phase = auth::AuthFlowPhase::Idle;
|
||||
}
|
||||
}
|
||||
|
||||
fn credential_state(&self, profile_id: &ProfileId) -> CredentialState {
|
||||
let profile_root = match default_profile_data_root() {
|
||||
Some(root) => root,
|
||||
None => return CredentialState::Unavailable,
|
||||
};
|
||||
let profile_dir = sync_profile_data_dir(&profile_root, profile_id);
|
||||
let store = auth::bearer_store_for_profile(
|
||||
profile_id,
|
||||
&profile_dir,
|
||||
&profile_root,
|
||||
self.default_profile_id.as_ref(),
|
||||
);
|
||||
match store.load() {
|
||||
Ok(Some(_)) => CredentialState::Present,
|
||||
Ok(None) => CredentialState::Missing,
|
||||
Err(error) => {
|
||||
tracing::warn!(target: "ely::sync", error = %error, "expired session credential probe failed");
|
||||
CredentialState::Unavailable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expiry_decision(
|
||||
active_matches: bool,
|
||||
is_current_generation: bool,
|
||||
sign_out_active: bool,
|
||||
credential: CredentialState,
|
||||
preserve_auth_attempt: bool,
|
||||
) -> ExpiryDecision {
|
||||
if !active_matches {
|
||||
return ExpiryDecision::Ignore;
|
||||
}
|
||||
if credential == CredentialState::Present {
|
||||
return if is_current_generation && !sign_out_active {
|
||||
ExpiryDecision::RefreshReplacement { preserve_generation: preserve_auth_attempt }
|
||||
} else {
|
||||
ExpiryDecision::Ignore
|
||||
};
|
||||
}
|
||||
if sign_out_active && credential == CredentialState::Unavailable {
|
||||
return ExpiryDecision::Ignore;
|
||||
}
|
||||
let connection = match credential {
|
||||
CredentialState::Present => return ExpiryDecision::Ignore,
|
||||
CredentialState::Missing => SyncConnectionState::SignedOut,
|
||||
CredentialState::Unavailable => SyncConnectionState::CredentialUnavailable {
|
||||
message: "System credential access failed.".to_string(),
|
||||
},
|
||||
};
|
||||
ExpiryDecision::Reconcile {
|
||||
connection,
|
||||
preserve_generation: preserve_auth_attempt,
|
||||
finish_sign_out: sign_out_active,
|
||||
}
|
||||
}
|
||||
|
||||
fn preserves_auth_attempt(phase: &auth::AuthFlowPhase, profile_id: &ProfileId) -> bool {
|
||||
!matches!(phase, auth::AuthFlowPhase::Idle) && phase.belongs_to(profile_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn expiry_decision_covers_profile_aba_replacement_and_sign_out() {
|
||||
assert_eq!(
|
||||
expiry_decision(false, false, false, CredentialState::Missing, false),
|
||||
ExpiryDecision::Ignore
|
||||
);
|
||||
assert_eq!(
|
||||
expiry_decision(true, true, false, CredentialState::Present, false),
|
||||
ExpiryDecision::RefreshReplacement { preserve_generation: false }
|
||||
);
|
||||
assert_eq!(
|
||||
expiry_decision(true, false, false, CredentialState::Present, false),
|
||||
ExpiryDecision::Ignore
|
||||
);
|
||||
assert_eq!(
|
||||
expiry_decision(true, true, true, CredentialState::Unavailable, false),
|
||||
ExpiryDecision::Ignore
|
||||
);
|
||||
assert_eq!(
|
||||
expiry_decision(true, false, true, CredentialState::Missing, false),
|
||||
ExpiryDecision::Reconcile {
|
||||
connection: SyncConnectionState::SignedOut,
|
||||
preserve_generation: false,
|
||||
finish_sign_out: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_credential_preserves_a_current_otp_generation() {
|
||||
assert_eq!(
|
||||
expiry_decision(true, true, false, CredentialState::Present, true),
|
||||
ExpiryDecision::RefreshReplacement { preserve_generation: true }
|
||||
);
|
||||
assert_eq!(
|
||||
expiry_decision(true, false, false, CredentialState::Missing, true),
|
||||
ExpiryDecision::Reconcile {
|
||||
connection: SyncConnectionState::SignedOut,
|
||||
preserve_generation: true,
|
||||
finish_sign_out: false,
|
||||
}
|
||||
);
|
||||
assert!(matches!(
|
||||
expiry_decision(true, false, false, CredentialState::Unavailable, true),
|
||||
ExpiryDecision::Reconcile {
|
||||
connection: SyncConnectionState::CredentialUnavailable { .. },
|
||||
preserve_generation: true,
|
||||
finish_sign_out: false,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_otp_attempt_is_profile_scoped() {
|
||||
let profile_id = ProfileId::new();
|
||||
let phase = auth::AuthFlowPhase::Verifying {
|
||||
profile_id: profile_id.clone(),
|
||||
email: "user@example.com".to_string(),
|
||||
};
|
||||
|
||||
assert!(preserves_auth_attempt(&phase, &profile_id));
|
||||
assert!(!preserves_auth_attempt(&phase, &ProfileId::new()));
|
||||
assert!(!preserves_auth_attempt(&auth::AuthFlowPhase::Idle, &profile_id));
|
||||
}
|
||||
}
|
||||
@@ -80,8 +80,23 @@ fn credential_storage_failures_use_the_unavailable_update() {
|
||||
update,
|
||||
SyncStateUpdate::CredentialUnavailable {
|
||||
profile_id: owner,
|
||||
finishes_upload: true,
|
||||
..
|
||||
} if owner == profile_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_sessions_expire_upload_authentication() {
|
||||
let profile_id = ely_domain::ProfileId::new();
|
||||
for error in [
|
||||
ely_sync_client::SyncClientError::SessionEnded,
|
||||
ely_sync_client::SyncClientError::SessionChanged,
|
||||
] {
|
||||
let update = sync_failure_update(profile_id.clone(), error);
|
||||
|
||||
assert!(matches!(
|
||||
update,
|
||||
SyncStateUpdate::AuthenticationExpired { profile_id: owner } if owner == profile_id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user