278 lines
9.4 KiB
Rust
278 lines
9.4 KiB
Rust
use anyhow::Result;
|
|
use readur::test_utils::{TestContext, TestAuthHelper};
|
|
use readur::{seed, commands, models::{CreateUser, UserRole}};
|
|
|
|
/// Test that admin user is created with auto-generated password on first run
|
|
#[tokio::test]
|
|
async fn test_admin_seed_creates_user_with_auto_password() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
// Clear any env vars to test auto-generation
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
std::env::remove_var("ADMIN_USERNAME");
|
|
|
|
// Run seed
|
|
seed::seed_admin_user(&ctx.state.db).await?;
|
|
|
|
// Verify admin user exists
|
|
let admin = ctx.state.db.get_user_by_username("admin").await?
|
|
.expect("Admin user should exist");
|
|
|
|
// Verify role is Admin
|
|
assert_eq!(admin.role, UserRole::Admin, "User should have Admin role");
|
|
|
|
// Verify password is hashed (bcrypt format)
|
|
assert!(admin.password_hash.is_some(), "Password hash should exist");
|
|
let hash = admin.password_hash.unwrap();
|
|
assert!(
|
|
hash.starts_with("$2b$") || hash.starts_with("$2a$"),
|
|
"Password should be bcrypt hashed"
|
|
);
|
|
|
|
// Verify email format
|
|
assert_eq!(admin.email, "admin@readur.com", "Email should use default format");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|
|
|
|
/// Test that admin user is created with provided ADMIN_PASSWORD
|
|
#[tokio::test]
|
|
async fn test_admin_seed_uses_env_password() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
// Set password via environment variable
|
|
std::env::set_var("ADMIN_PASSWORD", "testpass123");
|
|
std::env::remove_var("ADMIN_USERNAME");
|
|
|
|
// Run seed
|
|
seed::seed_admin_user(&ctx.state.db).await?;
|
|
|
|
// Verify admin user exists
|
|
let admin = ctx.state.db.get_user_by_username("admin").await?
|
|
.expect("Admin user should exist");
|
|
|
|
// Verify we can login with the provided password
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let token = auth_helper.login_user("admin", "testpass123").await;
|
|
assert!(!token.is_empty(), "Should be able to login with provided password");
|
|
|
|
// Clean up env var
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|
|
|
|
/// Test that subsequent runs don't duplicate admin user
|
|
#[tokio::test]
|
|
async fn test_admin_seed_does_not_duplicate_user() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
std::env::remove_var("ADMIN_USERNAME");
|
|
|
|
// Run seed first time
|
|
seed::seed_admin_user(&ctx.state.db).await?;
|
|
let first_admin = ctx.state.db.get_user_by_username("admin").await?
|
|
.expect("Admin should exist after first seed");
|
|
|
|
// Run seed second time
|
|
seed::seed_admin_user(&ctx.state.db).await?;
|
|
let second_admin = ctx.state.db.get_user_by_username("admin").await?
|
|
.expect("Admin should still exist after second seed");
|
|
|
|
// Verify same user (same ID and hash)
|
|
assert_eq!(first_admin.id, second_admin.id, "Should be the same user");
|
|
assert_eq!(
|
|
first_admin.password_hash, second_admin.password_hash,
|
|
"Password should not have changed"
|
|
);
|
|
|
|
// Verify only one admin exists
|
|
let all_users = ctx.state.db.get_all_users().await?;
|
|
let admin_count = all_users.iter().filter(|u| u.username == "admin").count();
|
|
assert_eq!(admin_count, 1, "Should only have one admin user");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|
|
|
|
/// Test that user can successfully login with generated credentials
|
|
#[tokio::test]
|
|
async fn test_admin_seed_allows_login() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
// Set known password for testing
|
|
std::env::set_var("ADMIN_PASSWORD", "logintest123");
|
|
std::env::remove_var("ADMIN_USERNAME");
|
|
|
|
// Run seed
|
|
seed::seed_admin_user(&ctx.state.db).await?;
|
|
|
|
// Attempt login
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let token = auth_helper.login_user("admin", "logintest123").await;
|
|
|
|
// Verify token is not empty
|
|
assert!(!token.is_empty(), "Should receive valid JWT token");
|
|
|
|
// Verify token is valid by making authenticated request
|
|
let _response = auth_helper
|
|
.make_authenticated_request("GET", "/api/auth/me", None, &token)
|
|
.await;
|
|
|
|
// If we get here without panicking, the authenticated request succeeded
|
|
|
|
// Clean up env var
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|
|
|
|
/// Test that reset command generates new password and invalidates old one
|
|
#[tokio::test]
|
|
async fn test_reset_command_changes_password() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
// Create admin with known password
|
|
let _ = ctx.state.db.create_user(CreateUser {
|
|
username: "admin".to_string(),
|
|
email: "admin@readur.com".to_string(),
|
|
password: "oldpass123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
}).await?;
|
|
|
|
// Get original password hash
|
|
let old_hash = ctx.state.db.get_user_by_username("admin")
|
|
.await?
|
|
.unwrap()
|
|
.password_hash;
|
|
|
|
// Reset password with new one
|
|
std::env::set_var("ADMIN_PASSWORD", "newpass456");
|
|
commands::reset_admin_password(&ctx.state.db).await?;
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
|
|
// Get new password hash
|
|
let new_hash = ctx.state.db.get_user_by_username("admin")
|
|
.await?
|
|
.unwrap()
|
|
.password_hash;
|
|
|
|
// Verify password changed
|
|
assert_ne!(old_hash, new_hash, "Password hash should have changed");
|
|
|
|
// Verify new password works
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let token = auth_helper.login_user("admin", "newpass456").await;
|
|
assert!(!token.is_empty(), "Should be able to login with new password");
|
|
|
|
// Note: We don't explicitly test that old password doesn't work because
|
|
// the auth helper panics on login failure. The fact that new password works
|
|
// and the hash changed is sufficient verification.
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|
|
|
|
/// Test that reset command uses provided ADMIN_PASSWORD
|
|
#[tokio::test]
|
|
async fn test_reset_command_uses_env_password() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
// Create admin
|
|
let _ = ctx.state.db.create_user(CreateUser {
|
|
username: "admin".to_string(),
|
|
email: "admin@readur.com".to_string(),
|
|
password: "initial123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
}).await?;
|
|
|
|
// Reset with specific password
|
|
std::env::set_var("ADMIN_PASSWORD", "specific789");
|
|
commands::reset_admin_password(&ctx.state.db).await?;
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
|
|
// Verify can login with the specific password
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let token = auth_helper.login_user("admin", "specific789").await;
|
|
assert!(!token.is_empty(), "Should login with environment-specified password");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|
|
|
|
/// Test that reset command returns error for non-existent user
|
|
#[tokio::test]
|
|
async fn test_reset_command_fails_for_nonexistent_user() {
|
|
let ctx = TestContext::new().await;
|
|
let result: Result<()> = async {
|
|
// Don't create any admin user
|
|
|
|
// Try to reset password for non-existent admin
|
|
std::env::remove_var("ADMIN_USERNAME"); // Use default "admin"
|
|
std::env::set_var("ADMIN_PASSWORD", "testpass123");
|
|
|
|
let reset_result = commands::reset_admin_password(&ctx.state.db).await;
|
|
|
|
// Should return an error
|
|
assert!(
|
|
reset_result.is_err(),
|
|
"Reset should fail when user doesn't exist"
|
|
);
|
|
|
|
let error_message = reset_result.unwrap_err().to_string();
|
|
assert!(
|
|
error_message.contains("not found") || error_message.contains("Admin user"),
|
|
"Error should indicate user not found, got: {}",
|
|
error_message
|
|
);
|
|
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
std::env::remove_var("ADMIN_PASSWORD");
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
result.unwrap();
|
|
}
|