//! Test utilities for loading and working with test images and data //! //! This module provides utilities for loading test images from the tests/test_images/ //! directory and working with them in unit and integration tests. use std::path::Path; #[cfg(any(test, feature = "test-utils"))] use std::sync::Arc; #[cfg(any(test, feature = "test-utils"))] use crate::{AppState, models::UserResponse}; #[cfg(any(test, feature = "test-utils"))] use axum::Router; #[cfg(any(test, feature = "test-utils"))] use serde_json::json; #[cfg(any(test, feature = "test-utils"))] use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt}; #[cfg(any(test, feature = "test-utils"))] use testcontainers_modules::postgres::Postgres; #[cfg(any(test, feature = "test-utils"))] use tower::util::ServiceExt; /// Test image information with expected OCR content #[derive(Debug, Clone)] pub struct TestImage { pub filename: &'static str, pub path: String, pub mime_type: &'static str, pub expected_content: &'static str, } impl TestImage { pub fn new(filename: &'static str, mime_type: &'static str, expected_content: &'static str) -> Self { Self { filename, path: format!("tests/test_images/{}", filename), mime_type, expected_content, } } pub fn exists(&self) -> bool { Path::new(&self.path).exists() } pub async fn load_data(&self) -> Result, std::io::Error> { tokio::fs::read(&self.path).await } } /// Get all available test images with their expected OCR content pub fn get_test_images() -> Vec { vec![ TestImage::new("test1.png", "image/png", "Test 1\nThis is some text from text 1"), TestImage::new("test2.jpg", "image/jpeg", "Test 2\nThis is some text from text 2"), TestImage::new("test3.jpeg", "image/jpeg", "Test 3\nThis is some text from text 3"), TestImage::new("test4.png", "image/png", "Test 4\nThis is some text from text 4"), TestImage::new("test5.jpg", "image/jpeg", "Test 5\nThis is some text from text 5"), TestImage::new("test6.jpeg", "image/jpeg", "Test 6\nThis is some text from text 6"), TestImage::new("test7.png", "image/png", "Test 7\nThis is some text from text 7"), TestImage::new("test8.jpeg", "image/jpeg", "Test 8\nThis is some text from text 8"), TestImage::new("test9.png", "image/png", "Test 9\nThis is some text from text 9"), ] } /// Get a specific test image by number (1-9) pub fn get_test_image(number: u8) -> Option { if number < 1 || number > 9 { return None; } get_test_images().into_iter().nth((number - 1) as usize) } /// Load test image data by filename pub async fn load_test_image(filename: &str) -> Result, std::io::Error> { let path = format!("tests/test_images/{}", filename); tokio::fs::read(path).await } /// Check if test images directory exists and is accessible pub fn test_images_available() -> bool { Path::new("tests/test_images").exists() } /// Get available test images (only those that exist on filesystem) pub fn get_available_test_images() -> Vec { get_test_images() .into_iter() .filter(|img| img.exists()) .collect() } /// Skip test macro for conditional testing based on test image availability #[macro_export] macro_rules! skip_if_no_test_images { () => { if !crate::test_utils::test_images_available() { println!("Skipping test: test images directory not available"); return; } }; } /// Skip test macro for specific test image #[macro_export] macro_rules! skip_if_test_image_missing { ($image:expr) => { if !$image.exists() { println!("Skipping test: {} not found", $image.filename); return; } }; } #[cfg(test)] mod tests { use super::*; #[test] fn test_image_paths_are_valid() { let images = get_test_images(); assert_eq!(images.len(), 9); for (i, image) in images.iter().enumerate() { assert_eq!(image.filename, format!("test{}.{}", i + 1, if image.mime_type == "image/png" { "png" } else if image.filename.ends_with(".jpg") { "jpg" } else { "jpeg" } )); assert!(image.expected_content.starts_with(&format!("Test {}", i + 1))); } } #[test] fn test_get_specific_image() { let image1 = get_test_image(1).unwrap(); assert_eq!(image1.filename, "test1.png"); assert_eq!(image1.mime_type, "image/png"); assert!(image1.expected_content.contains("Test 1")); let image5 = get_test_image(5).unwrap(); assert_eq!(image5.filename, "test5.jpg"); assert_eq!(image5.mime_type, "image/jpeg"); assert!(image5.expected_content.contains("Test 5")); // Invalid numbers should return None assert!(get_test_image(0).is_none()); assert!(get_test_image(10).is_none()); } } /// Helper functions for integration tests #[cfg(any(test, feature = "test-utils"))] pub async fn create_test_app() -> (Router, ContainerAsync) { let postgres_image = Postgres::default() .with_env_var("POSTGRES_USER", "test") .with_env_var("POSTGRES_PASSWORD", "test") .with_env_var("POSTGRES_DB", "test"); let container = postgres_image.start().await.expect("Failed to start postgres container"); let port = container.get_host_port_ipv4(5432).await.expect("Failed to get postgres port"); // Use TEST_DATABASE_URL if available, otherwise use the container let database_url = std::env::var("TEST_DATABASE_URL") .unwrap_or_else(|_| format!("postgresql://test:test@localhost:{}/test", port)); let db = crate::db::Database::new(&database_url).await.unwrap(); db.migrate().await.unwrap(); let config = crate::config::Config { database_url: database_url.clone(), server_address: "127.0.0.1:0".to_string(), jwt_secret: "test-secret".to_string(), upload_path: "./test-uploads".to_string(), watch_folder: "./test-watch".to_string(), allowed_file_types: vec!["pdf".to_string(), "txt".to_string(), "png".to_string()], watch_interval_seconds: Some(30), file_stability_check_ms: Some(500), max_file_age_hours: None, // OCR Configuration ocr_language: "eng".to_string(), concurrent_ocr_jobs: 2, // Lower for tests ocr_timeout_seconds: 60, // Shorter for tests max_file_size_mb: 10, // Smaller for tests // Performance memory_limit_mb: 256, // Lower for tests cpu_priority: "normal".to_string(), }; let queue_service = Arc::new(crate::ocr_queue::OcrQueueService::new(db.clone(), db.pool.clone(), 2)); let state = Arc::new(AppState { db, config, webdav_scheduler: None, source_scheduler: None, queue_service, }); let app = Router::new() .nest("/api/auth", crate::routes::auth::router()) .nest("/api/documents", crate::routes::documents::router()) .nest("/api/search", crate::routes::search::router()) .nest("/api/settings", crate::routes::settings::router()) .nest("/api/users", crate::routes::users::router()) .nest("/api/ignored-files", crate::routes::ignored_files::ignored_files_routes()) .with_state(state); (app, container) } #[cfg(any(test, feature = "test-utils"))] pub async fn create_test_user(app: &Router) -> UserResponse { let user_data = json!({ "username": "testuser", "email": "test@example.com", "password": "password123" }); let response = app .clone() .oneshot( axum::http::Request::builder() .method("POST") .uri("/api/auth/register") .header("Content-Type", "application/json") .body(axum::body::Body::from(serde_json::to_vec(&user_data).unwrap())) .unwrap(), ) .await .unwrap(); let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); serde_json::from_slice(&body).unwrap() } #[cfg(any(test, feature = "test-utils"))] pub async fn create_admin_user(app: &Router) -> UserResponse { let admin_data = json!({ "username": "adminuser", "email": "admin@example.com", "password": "adminpass123", "role": "admin" }); let response = app .clone() .oneshot( axum::http::Request::builder() .method("POST") .uri("/api/auth/register") .header("Content-Type", "application/json") .body(axum::body::Body::from(serde_json::to_vec(&admin_data).unwrap())) .unwrap(), ) .await .unwrap(); let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); serde_json::from_slice(&body).unwrap() } #[cfg(any(test, feature = "test-utils"))] pub async fn login_user(app: &Router, username: &str, password: &str) -> String { let login_data = json!({ "username": username, "password": password }); let response = app .clone() .oneshot( axum::http::Request::builder() .method("POST") .uri("/api/auth/login") .header("Content-Type", "application/json") .body(axum::body::Body::from(serde_json::to_vec(&login_data).unwrap())) .unwrap(), ) .await .unwrap(); let body = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let login_response: serde_json::Value = serde_json::from_slice(&body).unwrap(); login_response["token"].as_str().unwrap().to_string() }