Files
nx9-auth/tests/audit_export_test.rs

279 lines
7.3 KiB
Rust

#![cfg(feature = "sqlite")]
use nx9_auth::db::{
self,
provider::{DatabaseProvider, SqliteProvider},
};
use std::sync::Arc;
async fn setup_test_provider() -> (Arc<SqliteProvider>, String) {
let db_id = uuid::Uuid::new_v4().to_string();
let db_path = format!("target/test_audit_export_{}.db", db_id);
let pool = db::create_pool(&db_path)
.await
.expect("Failed to create test pool");
db::run_migrations(&pool)
.await
.expect("Failed to run migrations");
(Arc::new(SqliteProvider::new(pool)), db_path)
}
async fn teardown_test_db(path: String) {
let _ = std::fs::remove_file(path);
}
#[tokio::test]
async fn test_audit_list_remains_clamped_to_500() {
let (provider, db_path) = setup_test_provider().await;
// Insert 600 audit events
for i in 0..600 {
let audit_id = uuid::Uuid::new_v4().to_string();
provider
.audit()
.insert(
&audit_id,
None,
None,
"user.login",
"user",
None,
"info",
Some("127.0.0.1"),
Some("TestRunner"),
Some(&format!("{{\"index\": {i}}}")),
)
.await
.unwrap();
}
// Normal list filter with limit=1000 MUST be clamped to 500 by backend API logic
let filter = nx9_auth::db::models::AuditFilter {
limit: 1000,
..Default::default()
};
let clamped_limit = filter.limit.clamp(1, 500);
assert_eq!(clamped_limit, 500);
let entries = provider
.audit()
.list_filtered(&nx9_auth::db::models::AuditFilter {
limit: clamped_limit,
..Default::default()
})
.await
.unwrap();
assert_eq!(
entries.len(),
500,
"Normal audit listing must be bounded to 500 records max"
);
teardown_test_db(db_path).await;
}
#[tokio::test]
async fn test_audit_export_can_return_more_than_500_up_to_5000() {
let (provider, db_path) = setup_test_provider().await;
// Insert 600 audit events
for i in 0..600 {
let audit_id = uuid::Uuid::new_v4().to_string();
provider
.audit()
.insert(
&audit_id,
None,
None,
"user.login",
"user",
None,
"info",
Some("127.0.0.1"),
Some("TestRunner"),
Some(&format!("{{\"index\": {i}}}")),
)
.await
.unwrap();
}
// Export query path with limit=5000 returns all 600 records (>500)
let export_filter = nx9_auth::db::models::AuditFilter {
limit: 5000,
..Default::default()
};
let entries = provider
.audit()
.list_filtered(&export_filter)
.await
.unwrap();
assert_eq!(
entries.len(),
600,
"Export query path must return >500 records when available"
);
teardown_test_db(db_path).await;
}
#[tokio::test]
async fn test_audit_export_hard_bounded_at_5000() {
let (_provider, db_path) = setup_test_provider().await;
// Request limit 999999 must be clamped to hard maximum 5000
let requested_limit = 999999i64;
let export_limit = requested_limit.clamp(1, 5000);
assert_eq!(export_limit, 5000);
teardown_test_db(db_path).await;
}
#[tokio::test]
async fn test_server_side_success_filtering_before_limit_and_count() {
let (provider, db_path) = setup_test_provider().await;
// Insert 10 success events and 10 failure events
for _ in 0..10 {
let audit_id = uuid::Uuid::new_v4().to_string();
provider
.audit()
.insert(
&audit_id,
None,
None,
"user.login",
"user",
None,
"info",
Some("127.0.0.1"),
Some("TestRunner"),
None,
)
.await
.unwrap();
}
for _ in 0..10 {
let audit_id = uuid::Uuid::new_v4().to_string();
provider
.audit()
.insert(
&audit_id,
None,
None,
"auth.failed",
"user",
None,
"warning",
Some("127.0.0.1"),
Some("TestRunner"),
None,
)
.await
.unwrap();
}
// Server-side filter success = true
let success_filter = nx9_auth::db::models::AuditFilter {
success: Some(true),
limit: 50,
..Default::default()
};
let count = provider
.audit()
.count_filtered(&success_filter)
.await
.unwrap();
let entries = provider
.audit()
.list_filtered(&success_filter)
.await
.unwrap();
assert_eq!(count, 10);
assert_eq!(entries.len(), 10);
assert!(entries.iter().all(|e| !e.action.contains("fail")));
// Server-side filter success = false
let fail_filter = nx9_auth::db::models::AuditFilter {
success: Some(false),
limit: 50,
..Default::default()
};
let fail_count = provider.audit().count_filtered(&fail_filter).await.unwrap();
let fail_entries = provider.audit().list_filtered(&fail_filter).await.unwrap();
assert_eq!(fail_count, 10);
assert_eq!(fail_entries.len(), 10);
assert!(fail_entries.iter().all(|e| e.action.contains("fail")));
teardown_test_db(db_path).await;
}
#[test]
fn test_rfc4180_csv_escaping_rules() {
let esc = |s: &str| format!("\"{}\"", s.replace('"', "\"\""));
assert_eq!(esc("simple"), "\"simple\"");
assert_eq!(esc("with,comma"), "\"with,comma\"");
assert_eq!(esc("with \"quotes\""), "\"with \"\"quotes\"\"\"");
assert_eq!(esc("multi\nline"), "\"multi\nline\"");
}
#[tokio::test]
async fn test_exact_resource_id_filter_excludes_generic_text_matches() {
let (provider, db_path) = setup_test_provider().await;
let tenant_id = "tenant-exact";
provider
.audit()
.insert(
&uuid::Uuid::new_v4().to_string(),
None,
None,
"tenant.updated",
"tenant",
Some(tenant_id),
"info",
None,
None,
Some("{}"),
)
.await
.unwrap();
provider
.audit()
.insert(
&uuid::Uuid::new_v4().to_string(),
None,
None,
"tenant.updated",
"tenant",
Some("other-tenant"),
"info",
None,
None,
Some(&format!("{{\"mentioned\":\"{tenant_id}\"}}")),
)
.await
.unwrap();
let filter = nx9_auth::db::models::AuditFilter {
resource_type: Some("tenant".into()),
resource_id: Some(tenant_id.into()),
limit: 50,
..Default::default()
};
assert_eq!(provider.audit().count_filtered(&filter).await.unwrap(), 1);
let entries = provider.audit().list_filtered(&filter).await.unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].resource_id.as_deref(), Some(tenant_id));
teardown_test_db(db_path).await;
}