use crate::error::VecboostError;
use dbnexus::{DbPool as NexusDbPool, Session};
#[derive(Clone)]
pub struct DbPool {
inner: NexusDbPool,
}
impl DbPool {
pub async fn new(url: &str) -> Result<Self, VecboostError> {
let pool = NexusDbPool::new(url)
.await
.map_err(|e| VecboostError::InternalError(format!("Failed to create db pool: {e}")))?;
Ok(Self { inner: pool })
}
pub async fn get_session(&self, role: &str) -> Result<Session, VecboostError> {
self.inner
.get_session(role)
.await
.map_err(|e| VecboostError::InternalError(format!("Failed to get session: {e}")))
}
pub fn inner(&self) -> &NexusDbPool {
&self.inner
}
}
pub async fn init_schema(pool: &DbPool) -> Result<(), VecboostError> {
let session = pool.get_session("admin").await?;
session
.execute_raw_ddl(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
permissions TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
.await
.map_err(|e| VecboostError::InternalError(format!("Failed to create users table: {e}")))?;
session
.execute_raw_ddl(
"CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
user TEXT,
ip TEXT,
request_id TEXT,
user_agent TEXT,
details TEXT NOT NULL DEFAULT '{}',
success INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
.await
.map_err(|e| {
VecboostError::InternalError(format!("Failed to create audit_logs table: {e}"))
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_pool_memory_sqlite() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create in-memory SQLite pool");
let status = pool.inner().status();
assert_eq!(status.total, 0, "New pool should have 0 connections");
}
#[tokio::test]
async fn test_get_session_admin_role() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create pool");
let session = pool
.get_session("admin")
.await
.expect("Failed to get admin session");
assert_eq!(session.role(), "admin");
}
#[tokio::test]
async fn test_get_session_user_role_rejected() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create pool");
let result = pool.get_session("user").await;
assert!(
result.is_err(),
"user role should be rejected without explicit permission config"
);
}
#[tokio::test]
async fn test_init_schema_creates_tables() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create pool");
init_schema(&pool).await.expect("Failed to init schema");
let session = pool
.get_session("admin")
.await
.expect("Failed to get session");
let result = session
.execute_raw("SELECT COUNT(*) FROM users")
.await
.expect("Failed to query users table");
assert!(result.rows_affected() == 0 || result.rows_affected() == 1);
let result = session
.execute_raw("SELECT COUNT(*) FROM audit_logs")
.await
.expect("Failed to query audit_logs table");
assert!(result.rows_affected() == 0 || result.rows_affected() == 1);
}
#[tokio::test]
async fn test_users_crud() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create pool");
init_schema(&pool).await.expect("Failed to init schema");
let session = pool
.get_session("admin")
.await
.expect("Failed to get session");
session
.execute_raw(
"INSERT INTO users (username, password_hash, role) VALUES ('testuser', 'hash123', 'admin')",
)
.await
.expect("Failed to insert user");
let select_result = session
.execute_raw("SELECT username FROM users WHERE username = 'testuser'")
.await
.expect("Failed to select user");
assert_eq!(select_result.rows_affected(), 1);
session
.execute_raw("UPDATE users SET role = 'user' WHERE username = 'testuser'")
.await
.expect("Failed to update user");
let delete_result = session
.execute_raw("DELETE FROM users WHERE username = 'testuser'")
.await
.expect("Failed to delete user");
assert_eq!(delete_result.rows_affected(), 1);
}
#[tokio::test]
async fn test_audit_logs_insert() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create pool");
init_schema(&pool).await.expect("Failed to init schema");
let session = pool
.get_session("admin")
.await
.expect("Failed to get session");
session
.execute_raw(
"INSERT INTO audit_logs (event_type, user, ip, details, success) \
VALUES ('login_success', 'admin', '127.0.0.1', '{}', 1)",
)
.await
.expect("Failed to insert audit log");
let result = session
.execute_raw("SELECT COUNT(*) FROM audit_logs WHERE event_type = 'login_success'")
.await
.expect("Failed to query audit_logs");
assert_eq!(result.rows_affected(), 1);
}
#[tokio::test]
async fn test_pool_clone_shares_connections() {
let pool = DbPool::new("sqlite::memory:")
.await
.expect("Failed to create pool");
let cloned = pool.clone();
let session1 = pool
.get_session("admin")
.await
.expect("Failed to get session from original");
let session2 = cloned
.get_session("admin")
.await
.expect("Failed to get session from clone");
assert_eq!(session1.role(), session2.role());
}
}