From 1e66c9216cbe6ef4f350cd64d150373f67421ca7 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sat, 10 Oct 2026 18:23:10 +0530 Subject: [PATCH] feat(auth): enable offline mode accounts and launch without Microsoft account --- packages/oneclient_auth/src/service.rs | 4 -- packages/oneclient_auth/src/store.rs | 76 +++++++++++++++++++----- packages/oneclient_auth/tests/offline.rs | 43 +++++++++++--- 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/packages/oneclient_auth/src/service.rs b/packages/oneclient_auth/src/service.rs index 2ce183a2..a8e3b61a 100644 --- a/packages/oneclient_auth/src/service.rs +++ b/packages/oneclient_auth/src/service.rs @@ -383,10 +383,6 @@ impl AuthService { Err(err) => return Err(err), }; - if account.is_offline() && !self.has_microsoft_account().await { - return Err(AuthError::OfflineRequiresMicrosoft); - } - Ok(account) } diff --git a/packages/oneclient_auth/src/store.rs b/packages/oneclient_auth/src/store.rs index 65f4f10e..8b159da5 100644 --- a/packages/oneclient_auth/src/store.rs +++ b/packages/oneclient_auth/src/store.rs @@ -23,8 +23,13 @@ impl CredentialsStore { return Ok(Self::default()); } - match polyio::read_json(&path).await { - Ok(store) => Ok(store), + match polyio::read_json::(&path).await { + Ok(mut store) => { + if store.default_user.is_none() && !store.users.is_empty() { + store.default_user = store.users.keys().copied().next(); + } + Ok(store) + } Err(err) => { tracing::warn!("failed to read auth file: {err}"); Ok(Self::default()) @@ -91,22 +96,22 @@ impl CredentialsStore { } fn insert_offline_account(&mut self, username: String) -> AuthResult { - if !self.has_microsoft_account() { - return Err(AuthError::OfflineRequiresMicrosoft); - } - validate_offline_username(&username)?; - if self + if let Some(existing) = self .users .values() - .any(|u| u.username.eq_ignore_ascii_case(&username)) + .find(|u| u.username.eq_ignore_ascii_case(&username)) + .cloned() { - return Err(AuthError::DuplicateUsername { username }); + self.default_user = Some(existing.id); + return Ok(existing); } let account = offline_account(username); - self.users.insert(account.id, account.clone()); + let id = account.id; + self.users.insert(id, account.clone()); + self.default_user = Some(id); Ok(account) } @@ -205,10 +210,6 @@ impl CredentialsStore { return Ok(None); }; - if account.is_offline() && !self.has_microsoft_account() { - return Err(AuthError::OfflineRequiresMicrosoft); - } - Ok(Some(account)) } } @@ -222,3 +223,50 @@ pub(crate) fn is_transient_auth_error(err: &AuthError) -> bool { if source.is_connect() || source.is_timeout() ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_offline_account_creation_sets_default() { + let mut store = CredentialsStore { + users: HashMap::new(), + default_user: None, + }; + + let account = store.insert_offline_account("Player".to_string()).unwrap(); + assert_eq!(account.username, "Player"); + assert_eq!(store.default_user, Some(account.id)); + assert_eq!(store.users.len(), 1); + } + + #[test] + fn test_custom_username_switches_default() { + let mut store = CredentialsStore { + users: HashMap::new(), + default_user: None, + }; + + let p1 = store.insert_offline_account("Player".to_string()).unwrap(); + assert_eq!(store.default_user, Some(p1.id)); + + let p2 = store.insert_offline_account("Steve".to_string()).unwrap(); + assert_eq!(store.default_user, Some(p2.id)); + assert_eq!(store.users.len(), 2); + } + + #[test] + fn test_case_insensitive_username_reuse() { + let mut store = CredentialsStore { + users: HashMap::new(), + default_user: None, + }; + + let p1 = store.insert_offline_account("Steve".to_string()).unwrap(); + let p2 = store.insert_offline_account("steve".to_string()).unwrap(); + assert_eq!(p1.id, p2.id); + assert_eq!(store.users.len(), 1); + assert_eq!(store.default_user, Some(p1.id)); + } +} diff --git a/packages/oneclient_auth/tests/offline.rs b/packages/oneclient_auth/tests/offline.rs index c5b2bf98..be36ed76 100644 --- a/packages/oneclient_auth/tests/offline.rs +++ b/packages/oneclient_auth/tests/offline.rs @@ -28,13 +28,14 @@ fn offline_uuid_matches_vanilla_algorithm() { } #[tokio::test] -async fn offline_account_blocked_without_microsoft() { +async fn offline_account_allowed_without_microsoft() { isolate_launcher_dir(); let mut store = CredentialsStore::default(); - let err = store.add_offline_account("Steve".into()).unwrap_err(); + let offline = store.add_offline_account("Steve".into()).unwrap(); - assert!(err.to_string().contains("Microsoft")); + assert_eq!(offline.kind, AccountKind::Offline); + assert_eq!(offline.username, "Steve"); } #[tokio::test] @@ -51,7 +52,7 @@ async fn offline_account_allowed_when_microsoft_exists() { } #[tokio::test] -async fn default_offline_fails_when_last_microsoft_removed() { +async fn default_offline_works_when_last_microsoft_removed() { isolate_launcher_dir(); let mut store = CredentialsStore::default(); @@ -60,11 +61,13 @@ async fn default_offline_fails_when_last_microsoft_removed() { store.users.insert(msa_id, msa); let offline = store.add_offline_account("Steve".into()).unwrap(); - store.default_user = Some(offline.id); + let offline_id = offline.id; + store.default_user = Some(offline_id); store.users.remove(&msa_id); - let err = store.default_account().await.unwrap_err(); - assert!(err.to_string().contains("Microsoft")); + let account = store.default_account().await.unwrap().unwrap(); + assert_eq!(account.id, offline_id); + assert_eq!(account.kind, AccountKind::Offline); } #[test] @@ -147,3 +150,29 @@ fn offline_accounts_never_need_sign_in() { assert!(!account.needs_sign_in()); } + +#[tokio::test] +async fn offline_account_for_launch_succeeds_without_microsoft() { + isolate_launcher_dir(); + + let (events, _rx) = oneclient_events::EventBus::channel(); + let net = oneclient_net::RequestClient::new(oneclient_net::NetConfig::default()).expect("net client"); + let mut store = CredentialsStore::default(); + let offline = store.add_offline_account("Steve".into()).unwrap(); + let service = oneclient_auth::AuthService::with_store(store, net, events); + + let launched = service + .account_for_launch(offline.id) + .await + .expect("offline account must be allowed for launch without Microsoft account"); + assert_eq!(launched.id, offline.id); + assert_eq!(launched.username, "Steve"); + assert_eq!(launched.kind, AccountKind::Offline); + + let default_launched = service + .default_account_for_launch() + .await + .expect("default offline account must be allowed for launch") + .expect("default account should exist"); + assert_eq!(default_launched.id, offline.id); +}