Release: NX9-Auth v0.3.0
This commit is contained in:
1 parent
6a04d7f793
commit
d93f2cef95
92 files changed
+2418
-1143
No files matched your search
@@ -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
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "sqlite")]
|
||||
|
||||
use nx9_auth::db::{self, models::Tenant};
|
||||
|
||||
async fn setup_test_db() -> (sqlx::SqlitePool, String) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "sqlite")]
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, StatusCode, header},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user