Files
flalingo/src-tauri/tests/common/mod.rs

267 lines
8.5 KiB
Rust

use chrono::Utc;
use flalingo_lib::models::{
exercise::Exercise,
node::Node,
path::{Metadata, Path},
};
use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool};
use tokio::fs;
/// Test database utilities for creating and managing test databases
pub struct TestDb {
pub pool: SqlitePool,
pub db_url: String,
}
impl TestDb {
/// Create a new test database with a unique name
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let test_id = uuid::Uuid::new_v4().to_string();
let db_url = format!("sqlite:./test_dbs/test_{}.db", test_id);
// Ensure test_dbs directory exists
fs::create_dir_all("./test_dbs").await?;
// Create database if it doesn't exist
if !Sqlite::database_exists(&db_url).await? {
Sqlite::create_database(&db_url).await?;
}
let pool = SqlitePool::connect(&db_url).await?;
// Run migrations
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(TestDb { pool, db_url })
}
/// Close the database connection and delete the test database file
pub async fn cleanup(self) -> Result<(), Box<dyn std::error::Error>> {
self.pool.close().await;
// Extract file path from URL
let file_path = self.db_url.replace("sqlite:", "");
if tokio::fs::metadata(&file_path).await.is_ok() {
tokio::fs::remove_file(&file_path).await?;
}
Ok(())
}
/// Seed the database with test data
pub async fn seed_test_data(&self) -> Result<(), Box<dyn std::error::Error>> {
// Insert test path
sqlx::query("INSERT INTO path (id, title, description) VALUES (?, ?, ?)")
.bind("test_path_001")
.bind("Test Path")
.bind("A path for testing")
.execute(&self.pool)
.await?;
// Insert test metadata
sqlx::query(
"INSERT INTO pathMetadata (pathId, version, created_at, updated_at) VALUES (?, ?, ?, ?)"
)
.bind("test_path_001")
.bind("1.0.0")
.bind("2024-01-01T10:00:00Z")
.bind("2024-01-01T10:00:00Z")
.execute(&self.pool)
.await?;
// Insert test node
sqlx::query("INSERT INTO node (id, title, description, pathId) VALUES (?, ?, ?, ?)")
.bind(1_i64)
.bind("Test Node")
.bind("A node for testing")
.bind("test_path_001")
.execute(&self.pool)
.await?;
// Insert test exercises
sqlx::query(
"INSERT INTO exercise (id, ex_type, content, nodeId, pathId) VALUES (?, ?, ?, ?, ?)",
)
.bind(1_i64)
.bind("vocabulary")
.bind("{\"word\": \"Test\", \"translation\": \"Test\"}")
.bind(1_i64)
.bind("test_path_001")
.execute(&self.pool)
.await?;
sqlx::query(
"INSERT INTO exercise (id, ex_type, content, nodeId, pathId) VALUES (?, ?, ?, ?, ?)",
)
.bind(2_i64)
.bind("multiple_choice")
.bind("{\"question\": \"Test?\", \"options\": [\"A\", \"B\"], \"correct\": 0}")
.bind(1_i64)
.bind("test_path_001")
.execute(&self.pool)
.await?;
Ok(())
}
/// Clear all data from the test database
pub async fn clear_data(&self) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query("DELETE FROM exercise")
.execute(&self.pool)
.await?;
sqlx::query("DELETE FROM node").execute(&self.pool).await?;
sqlx::query("DELETE FROM pathMetadata")
.execute(&self.pool)
.await?;
sqlx::query("DELETE FROM path").execute(&self.pool).await?;
Ok(())
}
}
/// Helper functions for creating test data
pub mod test_data {
use super::*;
pub fn create_test_path() -> Path {
let now = Utc::now();
let metadata = vec![Metadata {
path_id: "test_path_001".to_string(),
version: "1.0.0".to_string(),
created_at: now,
updated_at: now,
}];
let exercises = vec![
Exercise {
id: 1,
ex_type: "vocabulary".to_string(),
content: r#"{"word": "Hallo", "translation": "Hello", "example": "Hallo, wie geht's?"}"#.to_string(),
node_id: 1,
},
Exercise {
id: 2,
ex_type: "multiple_choice".to_string(),
content: r#"{"question": "How do you say 'goodbye' in German?", "options": ["Tschüss", "Hallo", "Bitte", "Danke"], "correct": 0}"#.to_string(),
node_id: 1,
},
];
let nodes = vec![Node {
id: 1,
title: "Basic Greetings".to_string(),
description: "Learn essential German greetings".to_string(),
path_id: "test_path_001".to_string(),
exercises,
}];
Path {
id: "test_path_001".to_string(),
title: "German Basics Test".to_string(),
description: "A test path for demonstrating functionality".to_string(),
metadata,
nodes,
}
}
pub fn create_test_exercise(id: u32, node_id: u32) -> Exercise {
Exercise {
id,
ex_type: "vocabulary".to_string(),
content: format!(
r#"{{"word": "TestWord{}", "translation": "TestTranslation{}", "example": "This is test {}."}}"#,
id, id, id
),
node_id,
}
}
pub fn create_test_node(id: u32, path_id: &str) -> Node {
Node {
id,
title: format!("Test Node {}", id),
description: format!("Description for test node {}", id),
path_id: path_id.to_string(),
exercises: vec![create_test_exercise(id, id)],
}
}
pub fn create_test_metadata(path_id: &str, version: &str) -> Metadata {
let now = Utc::now();
Metadata {
path_id: path_id.to_string(),
version: version.to_string(),
created_at: now,
updated_at: now,
}
}
}
/// Test assertions and utilities
pub mod assertions {
use super::*;
pub fn assert_paths_equal(expected: &Path, actual: &Path) {
assert_eq!(expected.id, actual.id);
assert_eq!(expected.title, actual.title);
assert_eq!(expected.description, actual.description);
assert_eq!(expected.metadata.len(), actual.metadata.len());
assert_eq!(expected.nodes.len(), actual.nodes.len());
for (expected_node, actual_node) in expected.nodes.iter().zip(actual.nodes.iter()) {
assert_nodes_equal(expected_node, actual_node);
}
}
pub fn assert_nodes_equal(expected: &Node, actual: &Node) {
assert_eq!(expected.id, actual.id);
assert_eq!(expected.title, actual.title);
assert_eq!(expected.description, actual.description);
assert_eq!(expected.path_id, actual.path_id);
assert_eq!(expected.exercises.len(), actual.exercises.len());
for (expected_ex, actual_ex) in expected.exercises.iter().zip(actual.exercises.iter()) {
assert_exercises_equal(expected_ex, actual_ex);
}
}
pub fn assert_exercises_equal(expected: &Exercise, actual: &Exercise) {
assert_eq!(expected.ex_type, actual.ex_type);
assert_eq!(expected.content, actual.content);
assert_eq!(expected.node_id, actual.node_id);
// Note: IDs might be different after save/load, so we don't compare them
}
pub fn assert_metadata_equal(expected: &Metadata, actual: &Metadata) {
assert_eq!(expected.path_id, actual.path_id);
assert_eq!(expected.version, actual.version);
// Note: Timestamps might have slight differences, so we check they're close
let time_diff = (expected.created_at.timestamp() - actual.created_at.timestamp()).abs();
assert!(time_diff < 60, "Created timestamps too different");
}
}
/// Async test setup macro
#[macro_export]
macro_rules! async_test {
($test_name:ident, $test_body:expr) => {
#[tokio::test]
async fn $test_name() -> Result<(), Box<dyn std::error::Error>> {
let test_db = common::TestDb::new().await?;
let result = { $test_body(&test_db).await };
test_db.cleanup().await?;
result
}
};
}
/// Setup logging for tests
pub fn setup_test_logging() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
}