- 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>
178 lines
5.1 KiB
Rust
178 lines
5.1 KiB
Rust
//! Tests for Session and API Token repository operations.
|
|
|
|
use chrono::{Duration, Utc};
|
|
use nx9_wg_core::crypto::{generate_api_token, hash_password};
|
|
use nx9_wg_core::types::auth::{ApiToken, Session};
|
|
use nx9_wg_db::Store;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_session_lifecycle() {
|
|
let store = Store::connect_in_memory().await.expect("connect");
|
|
store.migrate().await.expect("migrate");
|
|
|
|
let pw_hash = hash_password("AdminPass123!").expect("hash");
|
|
store.create_admin("admin", &pw_hash).await.expect("admin");
|
|
|
|
let now = Utc::now().naive_utc();
|
|
let session_id = Uuid::new_v4().to_string();
|
|
|
|
let session = Session {
|
|
id: session_id.clone(),
|
|
admin_id: 1,
|
|
created_at: now,
|
|
expires_at: now + Duration::hours(24),
|
|
last_seen_at: Some(now),
|
|
ip_address: Some("10.0.0.5".to_string()),
|
|
user_agent: Some("TestAgent/1.0".to_string()),
|
|
};
|
|
|
|
store
|
|
.create_session(&session)
|
|
.await
|
|
.expect("create_session");
|
|
|
|
let fetched = store
|
|
.get_session(&session_id)
|
|
.await
|
|
.expect("get_session")
|
|
.expect("session found");
|
|
assert_eq!(fetched.id, session_id);
|
|
assert_eq!(fetched.admin_id, 1);
|
|
assert_eq!(fetched.ip_address.as_deref(), Some("10.0.0.5"));
|
|
|
|
// Touch session
|
|
store
|
|
.touch_session(&session_id)
|
|
.await
|
|
.expect("touch_session");
|
|
|
|
// Test delete expired sessions
|
|
let expired_id = Uuid::new_v4().to_string();
|
|
let expired_session = Session {
|
|
id: expired_id.clone(),
|
|
admin_id: 1,
|
|
created_at: now - Duration::hours(48),
|
|
expires_at: now - Duration::hours(24),
|
|
last_seen_at: None,
|
|
ip_address: None,
|
|
user_agent: None,
|
|
};
|
|
store
|
|
.create_session(&expired_session)
|
|
.await
|
|
.expect("expired session");
|
|
|
|
let deleted = store
|
|
.delete_expired_sessions()
|
|
.await
|
|
.expect("delete expired");
|
|
assert_eq!(deleted, 1);
|
|
assert!(store.get_session(&expired_id).await.expect("get").is_none());
|
|
assert!(store.get_session(&session_id).await.expect("get").is_some());
|
|
|
|
// Delete single session
|
|
store
|
|
.delete_session(&session_id)
|
|
.await
|
|
.expect("delete session");
|
|
assert!(store.get_session(&session_id).await.expect("get").is_none());
|
|
|
|
// Test delete_all_admin_sessions
|
|
let s1 = Session {
|
|
id: "s1".to_string(),
|
|
admin_id: 1,
|
|
created_at: now,
|
|
expires_at: now + Duration::hours(1),
|
|
last_seen_at: None,
|
|
ip_address: None,
|
|
user_agent: None,
|
|
};
|
|
let s2 = Session {
|
|
id: "s2".to_string(),
|
|
admin_id: 1,
|
|
created_at: now,
|
|
expires_at: now + Duration::hours(1),
|
|
last_seen_at: None,
|
|
ip_address: None,
|
|
user_agent: None,
|
|
};
|
|
store.create_session(&s1).await.expect("s1");
|
|
store.create_session(&s2).await.expect("s2");
|
|
|
|
let deleted_all = store
|
|
.delete_all_admin_sessions(1)
|
|
.await
|
|
.expect("delete all");
|
|
assert_eq!(deleted_all, 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_token_lifecycle() {
|
|
let store = Store::connect_in_memory().await.expect("connect");
|
|
store.migrate().await.expect("migrate");
|
|
|
|
let pw_hash = hash_password("AdminPass123!").expect("hash");
|
|
store.create_admin("admin", &pw_hash).await.expect("admin");
|
|
|
|
let (raw_token, token_hash) = generate_api_token();
|
|
let token_id = Uuid::new_v4().to_string();
|
|
let now = Utc::now().naive_utc();
|
|
|
|
let token = ApiToken {
|
|
id: token_id.clone(),
|
|
admin_id: 1,
|
|
name: "CI/CD Deployment Token".to_string(),
|
|
token_hash: token_hash.clone(),
|
|
created_at: now,
|
|
expires_at: Some(now + Duration::days(30)),
|
|
last_used_at: None,
|
|
revoked_at: None,
|
|
revoked: false,
|
|
};
|
|
|
|
store.create_token(&token).await.expect("create_token");
|
|
|
|
// Lookup by hash
|
|
let found = store
|
|
.find_token_by_hash(&token_hash)
|
|
.await
|
|
.expect("find by hash")
|
|
.expect("token found");
|
|
assert_eq!(found.id, token_id);
|
|
assert_eq!(found.name, "CI/CD Deployment Token");
|
|
assert!(!found.revoked);
|
|
|
|
// Verify raw token is never in the stored record
|
|
let debug_out = format!("{:?}", found);
|
|
assert!(debug_out.contains("[REDACTED]"));
|
|
assert!(!debug_out.contains(&raw_token));
|
|
|
|
// Mark token used
|
|
store.mark_token_used(&token_id).await.expect("mark used");
|
|
let after_use = store
|
|
.get_token(&token_id)
|
|
.await
|
|
.expect("get")
|
|
.expect("token");
|
|
assert!(after_use.last_used_at.is_some());
|
|
|
|
// List tokens
|
|
let tokens = store.list_tokens().await.expect("list tokens");
|
|
assert_eq!(tokens.len(), 1);
|
|
|
|
// Revoke token
|
|
store.revoke_token(&token_id).await.expect("revoke token");
|
|
let revoked = store
|
|
.get_token(&token_id)
|
|
.await
|
|
.expect("get")
|
|
.expect("token");
|
|
assert!(revoked.revoked);
|
|
assert!(revoked.revoked_at.is_some());
|
|
|
|
// Delete token
|
|
store.delete_token(&token_id).await.expect("delete token");
|
|
assert!(store.get_token(&token_id).await.expect("get").is_none());
|
|
}
|