use super::*;
impl AccountStore {
pub(crate) fn empty_for_test(path: PathBuf) -> Self {
Self {
accounts: Vec::new(),
path,
load_error: None,
}
}
}
#[test]
fn offline_uuid_has_valid_v3_shape() {
let uuid = offline_uuid("Steve");
let parts: Vec<&str> = uuid.split('-').collect();
assert_eq!(parts.len(), 5, "UUID must have 5 dash-separated parts");
assert_eq!(parts[0].len(), 8);
assert_eq!(parts[1].len(), 4);
assert_eq!(parts[2].len(), 4);
assert_eq!(parts[3].len(), 4);
assert_eq!(parts[4].len(), 12);
assert!(parts[2].starts_with('3'));
let first_nibble = u8::from_str_radix(&parts[3][..1], 16).unwrap();
assert!((0x8..=0xb).contains(&first_nibble));
}
#[test]
fn offline_uuid_is_pinned_for_known_names() {
assert_eq!(
offline_uuid("Steve"),
"7e0a3689-ed7c-347c-87cc-3689ed7c47cc"
);
assert_eq!(offline_uuid("Alex"), "a13d42a3-454f-3e39-a392-42a3454fe392");
}
#[test]
fn offline_uuid_different_for_different_names() {
assert_ne!(offline_uuid("Steve"), offline_uuid("Alex"));
}
#[test]
fn create_offline_account_fields() {
let acc = create_offline_account("TestPlayer");
assert_eq!(acc.username, "TestPlayer");
assert_eq!(acc.account_type, AccountType::Offline);
assert!(!acc.active);
assert!(acc.refresh_token.is_none());
assert_eq!(acc.uuid, offline_uuid("TestPlayer"));
}
fn make_store(dir: &std::path::Path) -> AccountStore {
AccountStore {
accounts: Vec::new(),
path: dir.join("accounts.json"),
load_error: None,
}
}
fn microsoft_account(name: &str) -> Account {
Account {
uuid: format!("00000000-0000-0000-0000-{:012}", name.len()),
username: name.to_owned(),
account_type: AccountType::Microsoft,
active: false,
refresh_token: Some("refresh".to_owned()),
cached_mc_token: None,
cached_mc_token_expires_at: None,
}
}
#[test]
fn store_add_first_becomes_active() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
assert_eq!(store.accounts.len(), 1);
assert!(store.accounts[0].active);
}
#[test]
fn store_add_second_stays_inactive() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
assert_eq!(store.accounts.len(), 2);
assert!(store.accounts[0].active);
assert!(!store.accounts[1].active);
}
#[test]
fn store_add_duplicate_uuid_replaces() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
let mut dup = create_offline_account("Alice");
dup.username = "AliceRenamed".to_owned();
dup.uuid = store.accounts[0].uuid.clone();
store.add(dup).unwrap();
assert_eq!(store.accounts.len(), 1);
assert_eq!(store.accounts[0].username, "AliceRenamed");
}
#[test]
fn store_active_account_none_when_empty() {
let tmp = tempfile::tempdir().unwrap();
let store = make_store(tmp.path());
assert!(store.active_account().is_none());
}
#[test]
fn store_has_microsoft_account_when_one_exists() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Offline")).unwrap();
assert!(!store.has_microsoft_account());
store.add(microsoft_account("Owner")).unwrap();
assert!(store.has_microsoft_account());
}
#[test]
fn store_active_account_returns_active() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
let active = store.active_account().unwrap();
assert_eq!(active.username, "Alice");
}
#[test]
fn store_set_active_changes_active() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
store.set_active(1).unwrap();
assert!(!store.accounts[0].active);
assert!(store.accounts[1].active);
}
#[test]
fn store_set_active_out_of_bounds_keeps_selection() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
store.set_active(99).unwrap();
assert!(
store.accounts[0].active,
"existing selection must survive a bad index"
);
assert!(!store.accounts[1].active);
}
#[test]
fn store_add_replacing_active_keeps_active() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
let mut dup = create_offline_account("Alice");
dup.username = "AliceRenamed".to_owned();
store.add(dup).unwrap();
assert!(
store.accounts.iter().any(|a| a.active),
"re-adding the active account must not orphan the selection"
);
let active = store.active_account().unwrap();
assert_eq!(active.username, "AliceRenamed");
}
#[test]
fn store_remove_activates_first_remaining() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
store.remove(0).unwrap();
assert_eq!(store.accounts.len(), 1);
assert_eq!(store.accounts[0].username, "Bob");
assert!(store.accounts[0].active);
}
#[test]
fn store_remove_out_of_bounds_noop() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.remove(5).unwrap();
assert_eq!(store.accounts.len(), 1);
}
#[test]
fn store_save_and_reload() {
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(create_offline_account("Alice")).unwrap();
store.add(create_offline_account("Bob")).unwrap();
store.save().unwrap();
let reloaded = AccountStore {
accounts: serde_json::from_str(
&std::fs::read_to_string(tmp.path().join("accounts.json")).unwrap(),
)
.unwrap(),
path: tmp.path().join("accounts.json"),
load_error: None,
};
assert_eq!(reloaded.accounts.len(), 2);
assert_eq!(reloaded.accounts[0].username, "Alice");
assert!(reloaded.accounts[0].active);
}
#[cfg(unix)]
#[test]
fn account_file_remains_owner_only_after_replacement() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let mut store = make_store(tmp.path());
store.add(microsoft_account("Owner")).unwrap();
let path = tmp.path().join("accounts.json");
assert_eq!(
std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
0o600
);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
store.add(create_offline_account("Player")).unwrap();
assert_eq!(
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
0o600
);
}
#[test]
fn invalid_account_file_cannot_be_overwritten_by_a_new_account() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("accounts.json");
std::fs::write(&path, b"{broken credentials").unwrap();
let mut store = AccountStore::load_from(path.clone());
assert!(store.check_loaded().is_err());
assert!(store.add(microsoft_account("Owner")).is_err());
assert!(store.accounts.is_empty());
assert_eq!(std::fs::read(path).unwrap(), b"{broken credentials");
}
#[test]
fn stale_account_mutations_preserve_new_credentials_and_selection() {
let tmp = tempfile::tempdir().unwrap();
let mut first = make_store(tmp.path());
first.add(microsoft_account("Owner")).unwrap();
first.add(create_offline_account("Player")).unwrap();
let mut stale = AccountStore::load_from(first.path.clone());
let uuid = first.accounts[0].uuid.clone();
first
.update_credentials(
&uuid,
Some("new-refresh".to_owned()),
"new-token",
Some(12345),
)
.unwrap();
first.add(create_offline_account("New")).unwrap();
stale.set_active(1).unwrap();
assert_eq!(stale.accounts.len(), 3);
assert_eq!(
stale.accounts[0].refresh_token.as_deref(),
Some("new-refresh")
);
first
.update_credentials(&uuid, None, "newer-token", Some(23456))
.unwrap();
assert_eq!(first.active_account().unwrap().username, "Player");
assert_eq!(
first.accounts[0].cached_mc_token.as_deref(),
Some("newer-token")
);
}