mod common; use chrono::Utc; use common::TestDb; use flalingo_lib::models::{ exercise::Exercise, node::Node, path::{Metadata, Path}, }; use flalingo_lib::repositories::{ metadata_repository::MetadataRepository, path_repository::PathRepository, repository_manager::RepositoryManager, }; // Helper function to create test data 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, } } 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, } } 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)], } } 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", "audio": "/audio/hallo.mp3", "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, "explanation": "Tschüss is the informal way to say goodbye."}"#.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 repository functionality".to_string(), metadata, nodes, } } #[tokio::test] async fn test_metadata_repository() -> Result<(), Box> { let test_db = TestDb::new().await?; let repo = MetadataRepository::new(&test_db.pool); // Create test metadata let metadata = create_test_metadata("metadata_test_path", "1.0.0"); // Save metadata repo.save_metadata(&metadata).await?; // Retrieve metadata let retrieved = repo.get_metadata_by_path_id("metadata_test_path").await?; assert_eq!(retrieved.len(), 1); assert_eq!(retrieved[0].path_id, "metadata_test_path"); assert_eq!(retrieved[0].version, "1.0.0"); // Update metadata let mut updated_metadata = metadata.clone(); updated_metadata.version = "1.1.0".to_string(); updated_metadata.updated_at = Utc::now(); repo.update_metadata(&updated_metadata).await?; let updated_retrieved = repo.get_metadata_by_path_id("metadata_test_path").await?; assert_eq!(updated_retrieved[0].version, "1.1.0"); // Delete metadata repo.delete_metadata_by_path_id("metadata_test_path") .await?; let delete_result = repo.get_metadata_by_path_id("metadata_test_path").await; assert!(delete_result.is_err()); test_db.cleanup().await?; Ok(()) } #[tokio::test] async fn test_path_repository_crud() -> Result<(), Box> { let test_db = TestDb::new().await?; let repo = PathRepository::new(&test_db.pool); // Create test path let test_path = create_test_path(); // Save path let saved_path_id = repo.save_path(test_path.clone()).await?; assert_eq!(saved_path_id, test_path.id); // Retrieve path let path_id_int = saved_path_id.parse::().unwrap(); let retrieved_path = repo.get_path_by_id(path_id_int).await?; assert_eq!(retrieved_path.id, test_path.id); assert_eq!(retrieved_path.title, test_path.title); assert_eq!(retrieved_path.description, test_path.description); assert_eq!(retrieved_path.metadata.len(), test_path.metadata.len()); assert_eq!(retrieved_path.nodes.len(), test_path.nodes.len()); // Test path exists let exists = repo.path_exists(path_id_int).await?; assert!(exists); // Get all paths let all_paths = repo.get_all_paths().await?; assert_eq!(all_paths.len(), 1); // Search by title let search_results = repo.get_paths_by_title("German").await?; assert_eq!(search_results.len(), 1); // Clone path let cloned_path_id = repo .clone_path(path_id_int, "cloned_test_path", "Cloned Test Path") .await?; let cloned_id_int = cloned_path_id.parse::().unwrap(); let cloned_path = repo.get_path_by_id(cloned_id_int).await?; assert_eq!(cloned_path.id, "cloned_test_path"); assert_eq!(cloned_path.title, "Cloned Test Path"); // Update path let mut updated_path = test_path.clone(); updated_path.title = "Updated Test Path".to_string(); repo.update_path(updated_path).await?; let updated_retrieved = repo.get_path_by_id(path_id_int).await?; assert_eq!(updated_retrieved.title, "Updated Test Path"); // Delete path repo.delete_path(path_id_int).await?; let exists_after_delete = repo.path_exists(path_id_int).await?; assert!(!exists_after_delete); test_db.cleanup().await?; Ok(()) } #[tokio::test] async fn test_repository_manager() -> Result<(), Box> { let test_db = TestDb::new().await?; let repo_manager = RepositoryManager::new(&test_db.pool); // Test health check let is_healthy = repo_manager.health_check().await?; assert!(is_healthy); // Test initial stats let initial_stats = repo_manager.get_stats().await?; assert_eq!(initial_stats.path_count, 0); assert!(initial_stats.is_empty()); // Create and save test path let test_path = create_test_path(); let path_id = repo_manager.paths().save_path(test_path.clone()).await?; let path_id_int = path_id.parse::().unwrap(); // Test updated stats let updated_stats = repo_manager.get_stats().await?; assert_eq!(updated_stats.path_count, 1); assert_eq!(updated_stats.node_count, 1); assert_eq!(updated_stats.exercise_count, 2); assert!(!updated_stats.is_empty()); // Test path statistics let path_stats = repo_manager.get_path_statistics(path_id_int).await?; assert_eq!(path_stats.node_count, 1); assert_eq!(path_stats.total_exercises, 2); assert_eq!(path_stats.exercise_types.len(), 2); // Test search functionality let search_results = repo_manager.search_paths("German").await?; assert_eq!(search_results.len(), 1); assert!(search_results[0].relevance_score > 0); // Test validation let validation_issues = repo_manager.validate_path_integrity(path_id_int).await?; assert!( validation_issues.is_empty(), "Valid path should have no issues" ); // Test JSON export/import let exported_json = repo_manager.export_path_to_json(path_id_int).await?; assert!(exported_json.contains(&test_path.id)); assert!(exported_json.contains(&test_path.title)); // Import as new path let modified_json = exported_json.replace("test_path_001", "imported_path_001"); let imported_path_id = repo_manager.import_path_from_json(&modified_json).await?; let imported_id_int = imported_path_id.parse::().unwrap(); let imported_path = repo_manager.paths().get_path_by_id(imported_id_int).await?; assert_eq!(imported_path.id, "imported_path_001"); // Test cloning let cloned_path_id = repo_manager .clone_path_complete(path_id_int, "cloned_manager_test", "Cloned Manager Test") .await?; let cloned_id_int = cloned_path_id.parse::().unwrap(); let cloned_path = repo_manager.paths().get_path_by_id(cloned_id_int).await?; assert_eq!(cloned_path.id, "cloned_manager_test"); test_db.cleanup().await?; Ok(()) } #[tokio::test] async fn test_transaction_handling() -> Result<(), Box> { let test_db = TestDb::new().await?; let repo_manager = RepositoryManager::new(&test_db.pool); // Test successful transaction { let mut tx = repo_manager.begin_transaction().await?; sqlx::query("INSERT INTO path (id, title, description) VALUES (?, ?, ?)") .bind("tx_test_path") .bind("Transaction Test") .bind("Testing transactions") .execute(&mut *tx) .await?; tx.commit().await.map_err(|e| e.to_string())?; } // Verify data was committed let path_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM path WHERE id = ?") .bind("tx_test_path") .fetch_one(&test_db.pool) .await?; assert_eq!(path_count.0, 1); // Test transaction rollback { let mut tx2 = repo_manager.begin_transaction().await?; sqlx::query("INSERT INTO path (id, title, description) VALUES (?, ?, ?)") .bind("rollback_test_path") .bind("Rollback Test") .bind("Testing rollback") .execute(&mut *tx2) .await?; // Drop transaction without committing (rollback) drop(tx2); } // Verify data was not committed let rollback_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM path WHERE id = ?") .bind("rollback_test_path") .fetch_one(&test_db.pool) .await?; assert_eq!(rollback_count.0, 0); test_db.cleanup().await?; Ok(()) } #[tokio::test] async fn test_error_handling() -> Result<(), Box> { let test_db = TestDb::new().await?; let repo_manager = RepositoryManager::new(&test_db.pool); // Test non-existent path retrieval let result = repo_manager.paths().get_path_by_id(999).await; assert!(result.is_err()); // Test non-existent path deletion let delete_result = repo_manager.paths().delete_path(999).await; assert!(delete_result.is_err()); // Test invalid JSON import let invalid_json = r#"{"invalid": "structure", "missing": "fields"}"#; let import_result = repo_manager.import_path_from_json(invalid_json).await; assert!(import_result.is_err()); // Test non-existent path export let export_result = repo_manager.export_path_to_json(999).await; assert!(export_result.is_err()); // Test non-existent path statistics let stats_result = repo_manager.get_path_statistics(999).await; assert!(stats_result.is_err()); // Test non-existent path validation let validation_result = repo_manager.validate_path_integrity(999).await; assert!(validation_result.is_err()); test_db.cleanup().await?; Ok(()) } #[tokio::test] async fn test_concurrent_operations() -> Result<(), Box> { let test_db = TestDb::new().await?; // Create multiple paths concurrently let mut handles = vec![]; for i in 0..5 { let pool_clone = test_db.pool.clone(); let mut test_path = create_test_path(); test_path.id = format!("concurrent_path_{}", i); test_path.title = format!("Concurrent Path {}", i); let handle = tokio::spawn(async move { let repo_manager = RepositoryManager::new(&pool_clone); repo_manager.paths().save_path(test_path).await }); handles.push(handle); } // Wait for all paths to be saved let mut successful_saves = 0; for handle in handles { match handle.await? { Ok(_) => successful_saves += 1, Err(e) => println!("Concurrent save failed: {}", e), } } assert_eq!(successful_saves, 5); // Verify all paths were saved let repo_manager = RepositoryManager::new(&test_db.pool); let all_paths = repo_manager.paths().get_all_paths().await?; assert_eq!(all_paths.len(), 5); test_db.cleanup().await?; Ok(()) } #[tokio::test] async fn test_complex_path_operations() -> Result<(), Box> { let test_db = TestDb::new().await?; let repo_manager = RepositoryManager::new(&test_db.pool); // Create a complex path with multiple nodes and exercises let mut complex_path = Path { id: "complex_test_path".to_string(), title: "Complex Test Path".to_string(), description: "A path with multiple nodes and exercises".to_string(), metadata: vec![create_test_metadata("complex_test_path", "1.0.0")], nodes: vec![], }; // Add multiple nodes with different exercise types for i in 1..=3 { let mut node = Node { id: i, title: format!("Node {}", i), description: format!("Description for node {}", i), path_id: "complex_test_path".to_string(), exercises: vec![], }; // Add different types of exercises to each node for j in 1..=2 { let exercise_id = (i - 1) * 2 + j; let exercise_type = match j { 1 => "vocabulary", 2 => "multiple_choice", _ => "fill_blank", }; let exercise = Exercise { id: exercise_id, ex_type: exercise_type.to_string(), content: format!( r#"{{"type": "{}", "content": "Exercise {} for node {}"}}"#, exercise_type, exercise_id, i ), node_id: i, }; node.exercises.push(exercise); } complex_path.nodes.push(node); } // Save complex path let path_id = repo_manager.paths().save_path(complex_path.clone()).await?; let path_id_int = path_id.parse::().unwrap(); // Retrieve and verify complex structure let retrieved_path = repo_manager.paths().get_path_by_id(path_id_int).await?; assert_eq!(retrieved_path.nodes.len(), 3); let total_exercises: usize = retrieved_path.nodes.iter().map(|n| n.exercises.len()).sum(); assert_eq!(total_exercises, 6); // Test statistics on complex path let stats = repo_manager.get_path_statistics(path_id_int).await?; assert_eq!(stats.node_count, 3); assert_eq!(stats.total_exercises, 6); assert_eq!(stats.avg_exercises_per_node, 2.0); // Test search across complex content let search_results = repo_manager.search_paths("Complex").await?; assert_eq!(search_results.len(), 1); test_db.cleanup().await?; Ok(()) }