- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization. - Create parent directories when an explicit --database path is provided. - Redact printed generated administrator passwords; announce file path or redact instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
81 lines
3.0 KiB
Rust
81 lines
3.0 KiB
Rust
//! Tests for Administrator repository operations and security invariants.
|
|
|
|
use nx9_wg_core::crypto::{hash_password, verify_password};
|
|
use nx9_wg_db::Store;
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_single_identity_and_crud() {
|
|
let store = Store::connect_in_memory().await.expect("connect");
|
|
store.migrate().await.expect("migrate");
|
|
|
|
// Initially no admin exists
|
|
assert!(!store.admin_exists().await.expect("admin_exists"));
|
|
assert!(store.get_admin().await.expect("get_admin").is_none());
|
|
|
|
// Create single admin with Argon2id hash
|
|
let password = "CorrectHorseBatteryStaple123!";
|
|
let password_hash = hash_password(password).expect("hash password");
|
|
let admin = store
|
|
.create_admin("admin", &password_hash)
|
|
.await
|
|
.expect("create_admin");
|
|
|
|
assert_eq!(admin.id, 1);
|
|
assert_eq!(admin.username, "admin");
|
|
assert!(!admin.totp_enabled);
|
|
assert!(admin.last_login_at.is_none());
|
|
|
|
// Verify admin_exists returns true
|
|
assert!(store.admin_exists().await.expect("admin_exists"));
|
|
|
|
// Verify lookup by username
|
|
let fetched = store
|
|
.get_admin_by_username("admin")
|
|
.await
|
|
.expect("get_admin_by_username")
|
|
.expect("admin found");
|
|
assert_eq!(fetched.id, 1);
|
|
assert!(verify_password(password, &fetched.password_hash).expect("verify password"));
|
|
|
|
// Reject second admin creation
|
|
let second_res = store.create_admin("admin2", "hash2").await;
|
|
assert!(second_res.is_err(), "second admin must be rejected");
|
|
|
|
// Test password change
|
|
let new_password = "NewSuperSecurePassword456!";
|
|
let new_hash = hash_password(new_password).expect("new hash");
|
|
store
|
|
.update_admin_password(&new_hash)
|
|
.await
|
|
.expect("update_admin_password");
|
|
|
|
let updated = store.get_admin().await.expect("get_admin").expect("admin");
|
|
assert!(verify_password(new_password, &updated.password_hash).expect("verify new"));
|
|
assert!(!verify_password(password, &updated.password_hash).expect("old password fails"));
|
|
|
|
// Test TOTP update
|
|
store
|
|
.update_admin_totp(Some("JBSWY3DPEHPK3PXP"), true)
|
|
.await
|
|
.expect("update_admin_totp");
|
|
let totp_admin = store.get_admin().await.expect("get_admin").expect("admin");
|
|
assert!(totp_admin.totp_enabled);
|
|
assert_eq!(totp_admin.totp_secret.as_deref(), Some("JBSWY3DPEHPK3PXP"));
|
|
|
|
// Test recording login
|
|
store
|
|
.record_admin_login(Some("192.168.1.100"))
|
|
.await
|
|
.expect("record_admin_login");
|
|
let login_admin = store.get_admin().await.expect("get_admin").expect("admin");
|
|
assert!(login_admin.last_login_at.is_some());
|
|
assert_eq!(login_admin.last_login_ip.as_deref(), Some("192.168.1.100"));
|
|
|
|
// Verify Debug formatting redacts password_hash and totp_secret
|
|
let debug_str = format!("{:?}", login_admin);
|
|
assert!(debug_str.contains("[REDACTED]"));
|
|
assert!(!debug_str.contains(password));
|
|
assert!(!debug_str.contains(new_password));
|
|
assert!(!debug_str.contains("JBSWY3DPEHPK3PXP"));
|
|
}
|