Files
nx9-url-shortener/tests/admin_user_management_tests.rs
T
thakares f49698bb5c
Rust CI / Test & Quality Checks (push) Canceled after 0s
Rust CI / Build Docker Image (push) Canceled after 0s
Release v0.6.0
2026-08-09 17:17:57 +05:30

210 lines
6.5 KiB
Rust

use axum::extract::{Form, Query, State};
use axum::http::HeaderMap;
use axum_extra::extract::cookie::{Cookie, CookieJar};
use chrono::Utc;
use rusqlite::Connection;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
use bzod::analytics::AnalyticsQueue;
use bzod::auth::hash_password;
use bzod::config::Config;
use bzod::db::users::{create_admin_user, get_user_by_username};
use bzod::db::Db;
use bzod::state::AppState;
use bzod::web::admin::{
users_create_post, users_delete_post, users_get, CreateUserForm, DeleteUserForm, UsersQuery,
};
fn create_temp_config(temp_dir: PathBuf) -> Config {
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.clone();
config.base_url = Some("http://localhost:8080".to_string());
config
}
fn build_state(config: Config) -> (Db, AppState) {
let db = Db::init(&config).expect("Failed to init Db");
let (tx, rx) = tokio::sync::watch::channel(false);
Box::leak(Box::new(tx));
let (queue, _) = AnalyticsQueue::new(db.clone(), 1000, rx);
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
db: db.clone(),
config,
analytics_queue: queue,
start_time: Instant::now(),
};
(db, state)
}
fn create_admin_session(conn: &Connection, admin_id: i64, session_id: &str) {
let now = Utc::now();
let expires_at = (now + chrono::Duration::hours(1)).to_rfc3339();
let created_at = now.to_rfc3339();
conn.execute(
"INSERT INTO sessions (id, user_id, expires_at, created_at) VALUES (?1, ?2, ?3, ?4);",
rusqlite::params![session_id, admin_id, expires_at, created_at],
)
.expect("Failed to insert admin session");
}
#[tokio::test]
async fn test_admin_users_page_and_user_creation() {
let temp_dir = std::env::temp_dir().join(format!(
"bzod_test_admin_user_management_{}",
uuid::Uuid::new_v4()
));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let (_db, state) = build_state(config.clone());
let password = "adminpassword";
let hash = hash_password(password).unwrap();
let admin_user = {
let conn = state.users_db.lock().unwrap();
create_admin_user(&conn, "admin", &hash).unwrap()
};
let session_token = "session-token-123";
{
let conn = state.users_db.lock().unwrap();
create_admin_session(&conn, admin_user.id, session_token);
}
let jar = CookieJar::new().add(Cookie::new("bzod_session", session_token));
let response = users_get(
State(state.clone()),
jar,
Query(UsersQuery {
success: None,
error: None,
}),
)
.await;
if response.status().is_redirection() {
let location = response
.headers()
.get("location")
.map(|v| v.to_str().unwrap_or(""));
println!("users_get redirect to: {:?}", location);
}
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), 65536)
.await
.unwrap();
let body_str = String::from_utf8_lossy(&body);
assert!(body_str.contains("Users Management"));
assert!(body_str.contains("Create New User"));
assert!(body_str.contains("admin"));
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_admin_create_and_delete_user() {
let temp_dir = std::env::temp_dir().join(format!(
"bzod_test_admin_user_management_delete_{}",
uuid::Uuid::new_v4()
));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let (_db, state) = build_state(config.clone());
let password = "adminpassword";
let hash = hash_password(password).unwrap();
let admin_user = {
let conn = state.users_db.lock().unwrap();
create_admin_user(&conn, "admin", &hash).unwrap()
};
let session_token = "session-token-456";
{
let conn = state.users_db.lock().unwrap();
create_admin_session(&conn, admin_user.id, session_token);
}
let jar = CookieJar::new().add(Cookie::new("bzod_session", session_token));
let form = CreateUserForm {
username: "testuser".to_string(),
password: "password123".to_string(),
account_type: "standard".to_string(),
metadata: "test metadata".to_string(),
csrf_token: "invalid".to_string(),
};
// invalid CSRF should redirect with error
let response = users_create_post(State(state.clone()), jar.clone(), Form(form)).await;
assert!(response.status().is_redirection());
assert!(response
.headers()
.get("location")
.unwrap()
.to_str()
.unwrap()
.contains("Invalid CSRF token"));
// build a valid CSRF token from admin session
let csrf_token = bzod::auth::generate_csrf_token(session_token);
let form = CreateUserForm {
username: "testuser".to_string(),
password: "password123".to_string(),
account_type: "standard".to_string(),
metadata: "test metadata".to_string(),
csrf_token: csrf_token.clone(),
};
let response = users_create_post(State(state.clone()), jar.clone(), Form(form)).await;
assert!(response.status().is_redirection());
assert_eq!(
response.headers().get("location").unwrap(),
"/admin/users?success=User created successfully"
);
let created_user_id = {
let conn = state.users_db.lock().unwrap();
let user = get_user_by_username(&conn, "testuser")
.unwrap()
.expect("created user should exist");
user.id
};
let delete_form = DeleteUserForm {
csrf_token: csrf_token.clone(),
};
let response = users_delete_post(
State(state.clone()),
jar.clone(),
HeaderMap::new(),
None,
axum::extract::Path(created_user_id),
Form(delete_form),
)
.await;
assert!(response.status().is_redirection());
assert_eq!(
response.headers().get("location").unwrap(),
"/admin/users?success=User deleted successfully"
);
{
let conn = state.users_db.lock().unwrap();
let user = get_user_by_username(&conn, "testuser").unwrap();
assert!(user.is_none());
}
let _ = fs::remove_dir_all(&temp_dir);
}