Release: NX9-Auth v0.3.0

This commit is contained in:
thakares committed 2026-07-22 19:36:22 +05:30
1 parent 6a04d7f793
commit d93f2cef95
92 files changed
+2418 -1143

No files matched your search

+19 -8
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "sqlite")]
//! Authentication security tests (OWASP-oriented).
use axum::{
@@ -66,7 +68,7 @@ async fn test_login_is_post_only() {
let (state, db_path) = setup().await;
let app = api::router::build(state);
// GET must not authenticate and must not be a login handler (405 or 404).
// 1. GET /api/v1/auth/login must return METHOD_NOT_ALLOWED (405).
let res = app
.clone()
.oneshot(
@@ -78,13 +80,22 @@ async fn test_login_is_post_only() {
)
.await
.unwrap();
assert!(
res.status() == StatusCode::METHOD_NOT_ALLOWED
|| res.status() == StatusCode::NOT_FOUND
|| res.status() == StatusCode::UNAUTHORIZED,
"GET login must not succeed: {}",
res.status()
);
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
// 2. GET /login?username=...&password=... must be sanitized with HTTP 303 See Other redirecting to /login without credentials.
let res_spa = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri("/login?username=sec_admin&password=super_secure_admin_passphrase_123")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res_spa.status(), StatusCode::SEE_OTHER);
assert_eq!(res_spa.headers().get(header::LOCATION).unwrap(), "/login");
// POST with JSON succeeds and returns access_token.
let res = app
+12 -7
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "sqlite")]
use nx9_auth::cli::{Commands, run};
use nx9_auth::config::Config;
use std::fs;
@@ -18,11 +20,14 @@ async fn test_path_expansion() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
let mut config = Config::default();
config.database.path = "~/test_subdir/test.db".to_string();
config.database.path = Some("~/test_subdir/test.db".to_string());
config.resolve_paths();
let expected = Path::new(&home).join("test_subdir/test.db");
assert_eq!(config.database.path, expected.to_string_lossy().to_string());
assert_eq!(
config.database.sqlite_path(),
expected.to_string_lossy().to_string()
);
}
#[tokio::test]
@@ -31,7 +36,7 @@ async fn test_backup_validation_and_integrity() {
setup_test_db(db_path);
let mut config = Config::default();
config.database.path = db_path.to_string();
config.database.path = Some(db_path.to_string());
// 1. Initialize DB and run migrations
let pool = nx9_auth::db::create_pool(db_path).await.unwrap();
@@ -114,7 +119,7 @@ async fn test_backup_validation_and_integrity() {
#[tokio::test]
async fn test_cli_config_path_json() {
let mut config = Config::default();
config.database.path = "test.db".to_string();
config.database.path = Some("test.db".to_string());
let res = run(Commands::ConfigPath { json: true }, config.clone()).await;
assert!(res.is_ok());
@@ -131,7 +136,7 @@ async fn test_cli_init_non_interactive() {
let _ = fs::remove_file(db_path);
let mut config = Config::default();
config.database.path = db_path.to_string();
config.database.path = Some(db_path.to_string());
// Run init command in non-interactive mode
let res = run(
@@ -177,7 +182,7 @@ async fn test_cli_init_skip_admin() {
let _ = fs::remove_file(db_path);
let mut config = Config::default();
config.database.path = db_path.to_string();
config.database.path = Some(db_path.to_string());
// Run init command with skip_admin
let res = run(
@@ -214,7 +219,7 @@ async fn test_cli_show_user_and_token() {
setup_test_db(db_path);
let mut config = Config::default();
config.database.path = db_path.to_string();
config.database.path = Some(db_path.to_string());
// 1. Init DB and seed user
let pool = nx9_auth::db::create_pool(db_path).await.unwrap();
+5 -1
View File
@@ -1,3 +1,4 @@
#![cfg(feature = "sqlite")]
#![allow(clippy::needless_borrow)]
use axum::{
body::Body,
@@ -182,7 +183,10 @@ fn test_config(db_path: String) -> Config {
cookie_secure: false,
production: false,
},
database: nx9_auth::config::DatabaseConfig { path: db_path },
database: nx9_auth::config::DatabaseConfig {
path: Some(db_path),
..Default::default()
},
security: test_security_config(),
audit: nx9_auth::config::AuditConfig { enabled: true },
..Default::default()
+2
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "sqlite")]
use nx9_auth::db::{self, models::Tenant};
async fn setup_test_db() -> (sqlx::SqlitePool, String) {
+2
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "sqlite")]
use axum::{
body::Body,
http::{Request, StatusCode, header},
+104
View File
@@ -0,0 +1,104 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use nx9_auth::config::Config;
use nx9_auth::runtime::{
Application, HookRegistry, RuntimeState, ShutdownHook, ShutdownPriority, WorkerManager,
};
struct TestHook {
name: &'static str,
priority: ShutdownPriority,
counter: Arc<AtomicUsize>,
sequence: Arc<tokio::sync::Mutex<Vec<&'static str>>>,
}
#[async_trait::async_trait]
impl ShutdownHook for TestHook {
fn name(&self) -> &'static str {
self.name
}
fn priority(&self) -> ShutdownPriority {
self.priority
}
async fn shutdown(&self) -> anyhow::Result<()> {
self.counter.fetch_add(1, Ordering::SeqCst);
let mut seq = self.sequence.lock().await;
seq.push(self.name);
Ok(())
}
}
#[tokio::test]
async fn test_runtime_application_builder() -> anyhow::Result<()> {
let mut config = Config::default();
config.server.host = "127.0.0.1".to_string();
config.server.port = 0; // OS assigned port
config.database.url = Some("sqlite::memory:".to_string());
let mut app = Application::builder(config).build().await?;
assert_eq!(app.state(), RuntimeState::Starting);
app.perform_shutdown().await?;
assert_eq!(app.state(), RuntimeState::Stopped);
Ok(())
}
#[tokio::test]
async fn test_shutdown_hook_execution_order() {
let counter = Arc::new(AtomicUsize::new(0));
let sequence = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let hook_last = TestHook {
name: "hook_last",
priority: ShutdownPriority::Last,
counter: counter.clone(),
sequence: sequence.clone(),
};
let hook_first = TestHook {
name: "hook_first",
priority: ShutdownPriority::First,
counter: counter.clone(),
sequence: sequence.clone(),
};
let hook_normal = TestHook {
name: "hook_normal",
priority: ShutdownPriority::Normal,
counter: counter.clone(),
sequence: sequence.clone(),
};
let mut registry = HookRegistry::new();
registry.register(Box::new(hook_last));
registry.register(Box::new(hook_first));
registry.register(Box::new(hook_normal));
assert_eq!(registry.len(), 3);
registry.execute_all().await;
assert_eq!(counter.load(Ordering::SeqCst), 3);
let seq = sequence.lock().await;
assert_eq!(*seq, vec!["hook_first", "hook_normal", "hook_last"]);
}
#[tokio::test]
async fn test_worker_manager_lifecycle() {
let mut mgr = WorkerManager::new();
let group = mgr.group("background-jobs");
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
group.spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
c.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(mgr.active_tasks(), 1);
mgr.shutdown_all(Duration::from_secs(2)).await;
assert_eq!(mgr.active_tasks(), 0);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
+6 -1
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "sqlite")]
use axum::{
body::Body,
http::{Request, StatusCode, header},
@@ -53,7 +55,10 @@ fn test_config(db_path: String) -> Config {
cookie_secure: false,
production: false,
},
database: nx9_auth::config::DatabaseConfig { path: db_path },
database: nx9_auth::config::DatabaseConfig {
path: Some(db_path),
..Default::default()
},
security: test_security_config(),
audit: nx9_auth::config::AuditConfig { enabled: true },
..Default::default()