use super::*;
use crate::test_utils::init_test_environment;
use serial_test::serial;
use crate::oauth2::{AccountSearchField, OAuth2Store};
use crate::passkey::{PasskeyCredential, PasskeyStore};
use crate::session::{SessionId, UserId, insert_test_session, insert_test_user};
use crate::userdb::{User as DbUser, UserStore};
fn create_test_user(id: &str, account: &str, label: &str) -> DbUser {
DbUser {
id: id.to_string(),
account: account.to_string(),
label: label.to_string(),
is_admin: false,
sequence_number: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
fn create_test_credential(id: &str, user_id: &str) -> PasskeyCredential {
let now = chrono::Utc::now();
PasskeyCredential {
sequence_number: None,
credential_id: id.to_string(),
user_id: user_id.to_string(),
public_key: String::new(),
aaguid: String::new(),
rp_id: String::new(),
counter: 0,
user: serde_json::from_str(
"{\"user_handle\":\"test\",\"name\":\"test\",\"displayName\":\"Test\"}",
)
.unwrap(),
created_at: now,
updated_at: now,
last_used_at: now,
}
}
fn create_test_oauth2_account(
id: &str,
user_id: &str,
provider: &str,
provider_user_id: &str,
) -> crate::OAuth2Account {
crate::OAuth2Account {
sequence_number: None,
id: id.to_string(),
user_id: user_id.to_string(),
provider: provider.to_string(),
provider_user_id: provider_user_id.to_string(),
name: "Test User".to_string(),
email: "test@example.com".to_string(),
picture: None,
metadata: serde_json::json!({}),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
#[serial]
#[tokio::test]
async fn test_update_user_account_success() {
use crate::test_utils::init_test_environment;
init_test_environment().await;
let user_id = "test-user";
let session_id = "test-session-user";
insert_test_user(
UserId::new(user_id.to_string()).expect("Valid test user ID"),
"old-account",
"Old Label",
false,
)
.await
.expect("Failed to create test user");
insert_test_session(
SessionId::new(session_id.to_string()).expect("Valid test session ID"),
UserId::new(user_id.to_string()).expect("Valid test user ID"),
"test-csrf",
3600,
)
.await
.expect("Failed to create test session");
let result = super::update_user_account(
SessionId::new(session_id.to_string()).expect("Valid session ID"), UserId::new(user_id.to_string()).expect("Valid user ID"), Some("new-account".to_string()),
Some("New Label".to_string()),
)
.await;
assert!(
result.is_ok(),
"update_user_account failed: {:?}",
result.err()
);
let updated_user_from_func = result.unwrap();
assert_eq!(updated_user_from_func.id, user_id);
assert_eq!(updated_user_from_func.account, "new-account");
assert_eq!(updated_user_from_func.label, "New Label");
let user_from_db =
UserStore::get_user(UserId::new(user_id.to_string()).expect("Valid user ID"))
.await
.expect("DB error getting user")
.expect("User not found in DB after update");
assert_eq!(user_from_db.account, "new-account");
assert_eq!(user_from_db.label, "New Label");
}
#[serial]
#[tokio::test]
async fn test_update_user_account_not_found() {
init_test_environment().await;
let session_user_id = "session-user";
let session_id = "test-session";
let nonexistent_user_id = "nonexistent-user";
insert_test_user(
UserId::new(session_user_id.to_string()).expect("Valid session user ID"),
"session@example.com",
"Session User",
false,
)
.await
.expect("Failed to create session user");
insert_test_session(
SessionId::new(session_id.to_string()).expect("Valid session ID"),
UserId::new(session_user_id.to_string()).expect("Valid session user ID"),
"test-csrf",
3600,
)
.await
.expect("Failed to create session");
let result = super::update_user_account(
SessionId::new(session_id.to_string()).expect("Valid session ID"), UserId::new(nonexistent_user_id.to_string()).expect("Valid nonexistent user ID"), Some("new-account".to_string()),
Some("New Label".to_string()),
)
.await;
assert!(result.is_err());
match result {
Err(CoordinationError::Unauthorized) => {
}
_ => panic!("Expected Unauthorized error, got {result:?}"),
}
}
#[serial]
#[tokio::test]
async fn test_delete_user_account_success() {
init_test_environment().await;
UserStore::init()
.await
.expect("Failed to initialize UserStore");
OAuth2Store::init()
.await
.expect("Failed to initialize OAuth2Store");
PasskeyStore::init()
.await
.expect("Failed to initialize PasskeyStore");
let timestamp = chrono::Utc::now().timestamp_millis();
let user_id_to_delete = format!("user-to-delete-{timestamp}");
let test_user = create_test_user(
&user_id_to_delete,
&format!("test-account-{timestamp}"),
"Test User",
);
UserStore::upsert_user(test_user.clone())
.await
.expect("Failed to insert user");
let cred1 = create_test_credential(&format!("credential-1-{timestamp}"), &user_id_to_delete);
let cred2 = create_test_credential(&format!("credential-2-{timestamp}"), &user_id_to_delete);
PasskeyStore::store_credential(
CredentialId::new(cred1.credential_id.clone()).expect("Valid credential ID"),
cred1.clone(),
)
.await
.expect("Failed to store cred1");
PasskeyStore::store_credential(
CredentialId::new(cred2.credential_id.clone()).expect("Valid credential ID"),
cred2.clone(),
)
.await
.expect("Failed to store cred2");
let oauth_acc1 = create_test_oauth2_account(
&format!("oauth-acc-1-{timestamp}"),
&user_id_to_delete,
"google",
&format!("google-id-1-{timestamp}"),
);
let oauth_acc2 = create_test_oauth2_account(
&format!("oauth-acc-2-{timestamp}"),
&user_id_to_delete,
"github",
&format!("github-id-1-{timestamp}"),
);
OAuth2Store::upsert_oauth2_account(oauth_acc1)
.await
.expect("Failed to upsert oauth_acc1");
OAuth2Store::upsert_oauth2_account(oauth_acc2)
.await
.expect("Failed to upsert oauth_acc2");
let session_id = format!("test-session-{timestamp}");
insert_test_session(
SessionId::new(session_id.clone()).expect("Valid session ID"),
UserId::new(user_id_to_delete.clone()).expect("Valid user ID"),
"test-csrf",
3600,
)
.await
.expect("Failed to create session");
let result = super::delete_user_account(
SessionId::new(session_id.clone()).expect("Valid session ID"),
UserId::new(user_id_to_delete.clone()).expect("Valid user ID"),
)
.await;
assert!(
result.is_ok(),
"delete_user_account failed: {:?}",
result.err()
);
let mut returned_credential_ids = result.unwrap();
returned_credential_ids.sort(); let expected_ids = vec![
format!("credential-1-{}", timestamp),
format!("credential-2-{}", timestamp),
];
let mut expected_sorted = expected_ids.clone();
expected_sorted.sort();
assert_eq!(returned_credential_ids, expected_sorted);
let user_from_db =
UserStore::get_user(UserId::new(user_id_to_delete.clone()).expect("Valid user ID"))
.await
.expect("DB error getting user");
assert!(user_from_db.is_none(), "User was not deleted from DB");
let passkeys_from_db = PasskeyStore::get_credentials_by(CredentialSearchField::UserId(
UserId::new(user_id_to_delete.clone()).expect("Valid user ID"),
))
.await
.expect("DB error getting passkeys");
assert!(passkeys_from_db.is_empty(), "Passkeys were not deleted");
let oauth_accounts_from_db = OAuth2Store::get_oauth2_accounts_by(AccountSearchField::UserId(
UserId::new(user_id_to_delete.clone()).expect("Valid user ID"),
))
.await
.expect("DB error getting oauth accounts");
assert!(
oauth_accounts_from_db.is_empty(),
"OAuth2 accounts were not deleted"
);
}
#[serial]
#[tokio::test]
async fn test_delete_user_account_not_found() {
init_test_environment().await;
let session_user_id = "session-user";
let session_id = "test-session";
let nonexistent_user_id = "nonexistent-user";
insert_test_user(
UserId::new(session_user_id.to_string()).expect("Valid session user ID"),
"session@example.com",
"Session User",
false,
)
.await
.expect("Failed to create session user");
insert_test_session(
SessionId::new(session_id.to_string()).expect("Valid session ID"),
UserId::new(session_user_id.to_string()).expect("Valid session user ID"),
"test-csrf",
3600,
)
.await
.expect("Failed to create session");
let result = super::delete_user_account(
SessionId::new(session_id.to_string()).expect("Valid session ID"),
UserId::new(nonexistent_user_id.to_string()).expect("Valid nonexistent user ID"),
)
.await;
assert!(result.is_err());
match result {
Err(CoordinationError::Unauthorized) => {
}
_ => panic!("Expected Unauthorized error, got {result:?}"),
}
}
#[serial]
#[tokio::test]
async fn test_gen_new_user_id_success() {
init_test_environment().await;
UserStore::init()
.await
.expect("Failed to initialize UserStore");
OAuth2Store::init()
.await
.expect("Failed to initialize OAuth2Store");
PasskeyStore::init()
.await
.expect("Failed to initialize PasskeyStore");
let result = super::gen_new_user_id().await;
assert!(result.is_ok(), "gen_new_user_id failed: {:?}", result.err());
let generated_id = result.unwrap();
assert!(!generated_id.is_empty(), "Generated ID is empty");
let user_from_db =
UserStore::get_user(UserId::new(generated_id.clone()).expect("Valid user ID"))
.await
.expect("DB error checking generated ID");
assert!(
user_from_db.is_none(),
"Generated ID was found in DB, but should be unique"
);
}
#[serial]
#[tokio::test]
async fn test_gen_new_user_id_max_retries() {
init_test_environment().await;
UserStore::init()
.await
.expect("Failed to initialize UserStore");
OAuth2Store::init()
.await
.expect("Failed to initialize OAuth2Store");
PasskeyStore::init()
.await
.expect("Failed to initialize PasskeyStore");
let timestamp = chrono::Utc::now().timestamp_millis();
let test_user1 = create_test_user(
&format!("fixed-uuid-1-{timestamp}"),
&format!("user1-{timestamp}@example.com"),
"Test User 1",
);
let test_user2 = create_test_user(
&format!("fixed-uuid-2-{timestamp}"),
&format!("user2-{timestamp}@example.com"),
"Test User 2",
);
let test_user3 = create_test_user(
&format!("fixed-uuid-3-{timestamp}"),
&format!("user3-{timestamp}@example.com"),
"Test User 3",
);
UserStore::upsert_user(test_user1.clone())
.await
.expect("Failed to insert test user 1");
UserStore::upsert_user(test_user2.clone())
.await
.expect("Failed to insert test user 2");
UserStore::upsert_user(test_user3.clone())
.await
.expect("Failed to insert test user 3");
{
let result = gen_new_user_id_with_mock(&[
&format!("fixed-uuid-1-{timestamp}"),
&format!("fixed-uuid-2-{timestamp}"),
&format!("fixed-uuid-3-{timestamp}"),
])
.await;
assert!(result.is_err());
if let Err(CoordinationError::Coordination(msg)) = result {
assert_eq!(
msg,
"Failed to generate a unique user ID after multiple attempts"
);
} else {
panic!("Expected CoordinationError::Coordination, got {result:?}");
}
}
{
let result = gen_new_user_id_with_mock(&[
&format!("fixed-uuid-1-{timestamp}"),
&format!("fixed-uuid-2-{timestamp}"),
&format!("fixed-uuid-4-{timestamp}"),
])
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), format!("fixed-uuid-4-{timestamp}"));
}
UserStore::delete_user(UserId::new(test_user1.id).expect("Valid user ID"))
.await
.ok();
UserStore::delete_user(UserId::new(test_user2.id).expect("Valid user ID"))
.await
.ok();
UserStore::delete_user(UserId::new(test_user3.id).expect("Valid user ID"))
.await
.ok();
}
#[serial]
#[tokio::test]
async fn test_first_user_escape_hatch_self_delete() {
use crate::test_utils::restore_first_user_after_deletion;
init_test_environment().await;
let timestamp = chrono::Utc::now().timestamp_millis();
let other_admin_id = format!("other-admin-escape-{timestamp}");
let other_admin = DbUser {
sequence_number: None,
id: other_admin_id.clone(),
account: format!("{other_admin_id}@example.com"),
label: "Other Admin".to_string(),
is_admin: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
UserStore::upsert_user(other_admin)
.await
.expect("Failed to create other admin");
let first_user_session_id = format!("session-escape-hatch-{timestamp}");
insert_test_session(
SessionId::new(first_user_session_id.clone()).expect("Valid session ID"),
UserId::new("first-user".to_string()).expect("Valid user ID"),
"test-csrf-token",
3600,
)
.await
.expect("Failed to create session for first user");
let result = super::delete_user_account(
SessionId::new(first_user_session_id).expect("Valid session ID"),
UserId::new("first-user".to_string()).expect("Valid user ID"),
)
.await;
assert!(
result.is_ok(),
"First user should be able to self-delete when other admins exist, got: {result:?}"
);
let deleted_user =
UserStore::get_user(UserId::new("first-user".to_string()).expect("Valid user ID"))
.await
.expect("Failed to query user");
assert!(
deleted_user.is_none(),
"First user should no longer exist after self-deletion"
);
restore_first_user_after_deletion().await;
UserStore::delete_user(UserId::new(other_admin_id).expect("Valid user ID"))
.await
.ok();
}
#[serial]
#[tokio::test]
async fn test_self_delete_last_admin_prevented() {
init_test_environment().await;
let first_user_session_id = "test-session-first-user-self-delete";
insert_test_session(
SessionId::new(first_user_session_id.to_string()).expect("Valid session ID"),
UserId::new("first-user".to_string()).expect("Valid user ID"),
"test-csrf-token",
3600,
)
.await
.expect("Failed to create session for first user");
let result = super::delete_user_account(
SessionId::new(first_user_session_id.to_string()).expect("Valid session ID"),
UserId::new("first-user".to_string()).expect("Valid user ID"),
)
.await;
assert!(
result.is_err(),
"Should not be able to self-delete the last admin"
);
match result {
Err(CoordinationError::Conflict(msg)) => {
assert!(
msg.contains("Cannot delete the last admin user"),
"Error message should mention last admin deletion, got: {msg}"
);
}
_ => panic!("Expected Conflict error about last admin, got {result:?}"),
}
}
async fn gen_new_user_id_with_mock(uuids: &[&str]) -> Result<String, CoordinationError> {
for uuid_index in 0..3 {
if uuid_index >= uuids.len() {
return Err(CoordinationError::Coordination(
"Mock UUID list exhausted".to_string(),
));
}
let id = uuids[uuid_index].to_string();
match UserStore::get_user(UserId::new(id.clone()).expect("Valid user ID")).await {
Ok(None) => return Ok(id), Ok(Some(_)) => continue, Err(e) => {
return Err(
CoordinationError::Database(format!("Failed to check user ID: {e}")).log(),
);
}
}
}
Err(CoordinationError::Coordination(
"Failed to generate a unique user ID after multiple attempts".to_string(),
)
.log())
}