feat(auth): enable offline mode accounts and launch without Microsoft account
This commit is contained in:
parent
b979feff0c
commit
1e66c9216c
3 changed files with 98 additions and 25 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ impl CredentialsStore {
|
|||
return Ok(Self::default());
|
||||
}
|
||||
|
||||
match polyio::read_json(&path).await {
|
||||
Ok(store) => Ok(store),
|
||||
match polyio::read_json::<Self>(&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<MinecraftAccount> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue