use async_trait::async_trait; use pinakes_plugin_api::wasm::{HttpRequest, HttpResponse, LogLevel, LogMessage}; use pinakes_plugin_api::{ Capabilities, EnvironmentCapability, Event, EventType, ExtractedMetadata, FilesystemCapability, HealthStatus, MediaTypeDefinition, NetworkCapability, Plugin, PluginContext, PluginError, PluginMetadata, PluginResult, SearchIndexItem, SearchQuery, SearchResult, SearchStats, ThumbnailFormat, ThumbnailInfo, ThumbnailOptions, }; use std::collections::HashMap; use std::path::PathBuf; struct TestPlugin { initialized: bool, shutdown: bool, health_status: HealthStatus, metadata: PluginMetadata, } impl TestPlugin { fn new() -> Self { Self { initialized: false, shutdown: false, health_status: HealthStatus { healthy: true, message: Some("OK".to_string()), metrics: HashMap::new(), }, metadata: PluginMetadata { id: "test-plugin".to_string(), name: "Test Plugin".to_string(), version: "1.0.0".to_string(), author: "Test Author".to_string(), description: "A test plugin".to_string(), api_version: "1.0".to_string(), capabilities_required: Capabilities::default(), }, } } } #[async_trait] impl Plugin for TestPlugin { fn metadata(&self) -> &PluginMetadata { &self.metadata } async fn initialize(&mut self, _context: PluginContext) -> PluginResult<()> { self.initialized = true; Ok(()) } async fn shutdown(&mut self) -> PluginResult<()> { self.shutdown = true; Ok(()) } async fn health_check(&self) -> PluginResult { Ok(self.health_status.clone()) } } #[tokio::test] async fn test_plugin_context_creation() { let context = PluginContext { data_dir: PathBuf::from("/data/test-plugin"), cache_dir: PathBuf::from("/cache/test-plugin"), config: HashMap::from([ ("enabled".to_string(), serde_json::json!(true)), ("max_items".to_string(), serde_json::json!(100)), ]), capabilities: Capabilities { filesystem: FilesystemCapability { read: vec![PathBuf::from("/data")], write: vec![PathBuf::from("/data")], }, network: NetworkCapability { enabled: true, allowed_domains: Some(vec!["api.example.com".to_string()]), }, environment: EnvironmentCapability { enabled: true, allowed_vars: Some(vec!["API_KEY".to_string()]), }, max_memory_bytes: Some(256 * 1024 * 1024), max_cpu_time_ms: Some(30000), }, }; assert_eq!(context.data_dir, PathBuf::from("/data/test-plugin")); assert_eq!(context.cache_dir, PathBuf::from("/cache/test-plugin")); assert_eq!( context.config.get("enabled").unwrap(), &serde_json::json!(true) ); assert!(context.capabilities.network.enabled); assert_eq!( context.capabilities.max_memory_bytes, Some(256 * 1024 * 1024) ); } #[tokio::test] async fn test_plugin_context_fields() { let context = PluginContext { data_dir: PathBuf::from("/custom/data"), cache_dir: PathBuf::from("/custom/cache"), config: HashMap::new(), capabilities: Capabilities::default(), }; assert_eq!(context.data_dir, PathBuf::from("/custom/data")); assert_eq!(context.cache_dir, PathBuf::from("/custom/cache")); } #[tokio::test] async fn test_plugin_lifecycle() { let mut plugin = TestPlugin::new(); assert!(!plugin.initialized); assert!(!plugin.shutdown); let context = PluginContext { data_dir: PathBuf::from("/data"), cache_dir: PathBuf::from("/cache"), config: HashMap::new(), capabilities: Capabilities::default(), }; plugin.initialize(context).await.unwrap(); assert!(plugin.initialized); let health = plugin.health_check().await.unwrap(); assert!(health.healthy); assert_eq!(health.message, Some("OK".to_string())); plugin.shutdown().await.unwrap(); assert!(plugin.shutdown); } #[tokio::test] async fn test_extracted_metadata_structure() { let metadata = ExtractedMetadata { title: Some("Test Document".to_string()), description: Some("A test document".to_string()), author: Some("John Doe".to_string()), created_at: Some("2024-01-15T10:30:00Z".to_string()), duration_secs: Some(120.5), width: Some(1920), height: Some(1080), file_size_bytes: Some(1_500_000), codec: Some("h264".to_string()), bitrate_kbps: Some(5000), custom_fields: HashMap::from([ ("color_space".to_string(), serde_json::json!("sRGB")), ("orientation".to_string(), serde_json::json!(90)), ]), tags: vec!["test".to_string(), "document".to_string()], }; assert_eq!(metadata.title, Some("Test Document".to_string())); assert_eq!(metadata.width, Some(1920)); assert_eq!(metadata.height, Some(1080)); assert_eq!(metadata.tags.len(), 2); assert_eq!(metadata.custom_fields.get("color_space").unwrap(), "sRGB"); } #[tokio::test] async fn test_search_query_serialization() { let query = SearchQuery { query_text: "nature landscape".to_string(), filters: HashMap::from([ ("type".to_string(), serde_json::json!("image")), ("year".to_string(), serde_json::json!(2023)), ]), limit: 50, offset: 0, }; let serialized = serde_json::to_string(&query).unwrap(); let deserialized: SearchQuery = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.query_text, "nature landscape"); assert_eq!(deserialized.limit, 50); assert_eq!(deserialized.offset, 0); assert_eq!(deserialized.filters.get("type").unwrap(), "image"); } #[tokio::test] async fn test_search_result_serialization() { let result = SearchResult { id: "media-123".to_string(), score: 0.95, highlights: vec!["matched content".to_string()], }; let serialized = serde_json::to_string(&result).unwrap(); let deserialized: SearchResult = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.id, "media-123"); assert!((deserialized.score - 0.95).abs() < 0.001); assert_eq!(deserialized.highlights.len(), 1); } #[tokio::test] async fn test_search_stats_serialization() { let stats = SearchStats { total_indexed: 10_000, index_size_bytes: 500_000_000, last_update: Some("2024-01-15T12:00:00Z".to_string()), }; let serialized = serde_json::to_string(&stats).unwrap(); let deserialized: SearchStats = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.total_indexed, 10_000); assert_eq!(deserialized.index_size_bytes, 500_000_000); assert!(deserialized.last_update.is_some()); } #[tokio::test] async fn test_thumbnail_options_serialization() { let options = ThumbnailOptions { width: 320, height: 240, quality: 85, format: ThumbnailFormat::Jpeg, }; let serialized = serde_json::to_string(&options).unwrap(); let deserialized: ThumbnailOptions = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.width, 320); assert_eq!(deserialized.height, 240); assert_eq!(deserialized.quality, 85); assert!(matches!(deserialized.format, ThumbnailFormat::Jpeg)); } #[tokio::test] async fn test_thumbnail_format_variants() { for format in [ ThumbnailFormat::Jpeg, ThumbnailFormat::Png, ThumbnailFormat::WebP, ] { let options = ThumbnailOptions { width: 100, height: 100, quality: 90, format, }; let serialized = serde_json::to_string(&options).unwrap(); let deserialized: ThumbnailOptions = serde_json::from_str(&serialized).unwrap(); assert!(matches!(deserialized.format, _)); } } #[tokio::test] async fn test_thumbnail_info_structure() { let info = ThumbnailInfo { path: PathBuf::from("/thumbnails/test.jpg"), width: 320, height: 240, file_size_bytes: 45_000, }; assert_eq!(info.path, PathBuf::from("/thumbnails/test.jpg")); assert_eq!(info.width, 320); assert_eq!(info.height, 240); assert_eq!(info.file_size_bytes, 45_000); } #[tokio::test] async fn test_media_type_definition() { let media_type = MediaTypeDefinition { id: "custom-image format".to_string(), name: "Custom Image Format".to_string(), category: "image".to_string(), extensions: vec!["cif".to_string(), "custom-img".to_string()], mime_types: vec!["image/x-custom".to_string()], icon: Some("image-cif".to_string()), }; assert_eq!(media_type.id, "custom-image format"); assert_eq!(media_type.category, "image"); assert_eq!(media_type.extensions.len(), 2); assert!(media_type.icon.is_some()); } #[tokio::test] async fn test_event_type_variants() { let variants: Vec = vec![ EventType::MediaImported, EventType::MediaUpdated, EventType::MediaDeleted, EventType::MediaTagged, EventType::MediaUntagged, EventType::CollectionCreated, EventType::CollectionUpdated, EventType::CollectionDeleted, EventType::ScanStarted, EventType::ScanCompleted, EventType::Custom("custom".to_string()), ]; for event_type in &variants { let serialized = serde_json::to_string(event_type).unwrap(); let _deserialized: EventType = serde_json::from_str(&serialized).unwrap(); } } #[tokio::test] async fn test_event_serialization() { let event = Event { event_type: EventType::MediaImported, timestamp: "2024-01-15T10:00:00Z".to_string(), data: HashMap::from([ ("path".to_string(), serde_json::json!("/media/test.jpg")), ("size".to_string(), serde_json::json!(1024)), ]), }; let serialized = serde_json::to_string(&event).unwrap(); let deserialized: Event = serde_json::from_str(&serialized).unwrap(); assert!(matches!(deserialized.event_type, EventType::MediaImported)); assert_eq!(deserialized.timestamp, "2024-01-15T10:00:00Z"); } #[tokio::test] async fn test_http_request_serialization() { let request = HttpRequest { method: "GET".to_string(), url: "https://api.example.com/data".to_string(), headers: HashMap::from([ ("Authorization".to_string(), "Bearer token".to_string()), ("Content-Type".to_string(), "application/json".to_string()), ]), body: None, }; let serialized = serde_json::to_string(&request).unwrap(); let deserialized: HttpRequest = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.method, "GET"); assert_eq!(deserialized.url, "https://api.example.com/data"); assert!(deserialized.body.is_none()); } #[tokio::test] async fn test_http_response_serialization() { let response = HttpResponse { status: 200, headers: HashMap::from([("Content-Type".to_string(), "application/json".to_string())]), body: b"{\"success\": true}".to_vec(), }; let serialized = serde_json::to_string(&response).unwrap(); let deserialized: HttpResponse = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.status, 200); assert_eq!(deserialized.body, b"{\"success\": true}"); } #[tokio::test] async fn test_log_message_serialization() { let message = LogMessage { level: LogLevel::Info, target: "plugin::metadata".to_string(), message: "Metadata extraction complete".to_string(), fields: HashMap::from([ ("file_count".to_string(), "42".to_string()), ("duration_ms".to_string(), "150".to_string()), ]), }; let serialized = serde_json::to_string(&message).unwrap(); let deserialized: LogMessage = serde_json::from_str(&serialized).unwrap(); assert!(matches!(deserialized.level, LogLevel::Info)); assert_eq!(deserialized.target, "plugin::metadata"); assert_eq!(deserialized.message, "Metadata extraction complete"); } #[tokio::test] async fn test_log_level_variants() { let levels = [ LogLevel::Trace, LogLevel::Debug, LogLevel::Info, LogLevel::Warn, LogLevel::Error, ]; for level in levels { let serialized = serde_json::to_string(&level).unwrap(); let _deserialized: LogLevel = serde_json::from_str(&serialized).unwrap(); } } #[tokio::test] async fn test_plugin_error_variants() { let errors: Vec = vec![ PluginError::InitializationFailed("WASM load failed".to_string()), PluginError::UnsupportedOperation("Custom search not implemented".to_string()), PluginError::InvalidInput("Invalid file path".to_string()), PluginError::IoError("File not found".to_string()), PluginError::MetadataExtractionFailed("Parse error".to_string()), PluginError::ThumbnailGenerationFailed("Format not supported".to_string()), PluginError::SearchBackendError("Index corrupted".to_string()), PluginError::PermissionDenied("Access denied to /data".to_string()), PluginError::ResourceLimitExceeded("Memory limit exceeded".to_string()), PluginError::Other("Unknown error".to_string()), ]; for error in errors { let serialized = serde_json::to_string(&error).unwrap(); let deserialized: PluginError = serde_json::from_str(&serialized).unwrap(); assert_eq!(format!("{}", error), format!("{}", deserialized)); } } #[tokio::test] async fn test_search_index_item_serialization() { let item = SearchIndexItem { id: "media-456".to_string(), title: Some("Summer Vacation".to_string()), description: Some("Photos from summer vacation 2023".to_string()), content: None, tags: vec![ "vacation".to_string(), "summer".to_string(), "photos".to_string(), ], media_type: "image/jpeg".to_string(), metadata: HashMap::from([ ("camera".to_string(), serde_json::json!("Canon EOS R5")), ("location".to_string(), serde_json::json!("Beach")), ]), }; let serialized = serde_json::to_string(&item).unwrap(); let deserialized: SearchIndexItem = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.id, "media-456"); assert_eq!(deserialized.title, Some("Summer Vacation".to_string())); assert_eq!(deserialized.tags.len(), 3); assert_eq!(deserialized.media_type, "image/jpeg"); } #[tokio::test] async fn test_health_status_variants() { let healthy = HealthStatus { healthy: true, message: Some("All systems operational".to_string()), metrics: HashMap::from([ ("items_processed".to_string(), 1000.0), ("avg_process_time_ms".to_string(), 45.5), ]), }; assert!(healthy.healthy); let unhealthy = HealthStatus { healthy: false, message: Some("Database connection failed".to_string()), metrics: HashMap::new(), }; assert!(!unhealthy.healthy); assert_eq!( unhealthy.message, Some("Database connection failed".to_string()) ); } #[tokio::test] async fn test_capabilities_merge() { let mut caps = Capabilities::default(); assert!(!caps.network.enabled); caps.network.enabled = true; caps.max_memory_bytes = Some(512 * 1024 * 1024); assert!(caps.network.enabled); assert_eq!(caps.max_memory_bytes, Some(512 * 1024 * 1024)); } #[tokio::test] async fn test_filesystem_capability_paths() { let caps = FilesystemCapability { read: vec![ PathBuf::from("/data"), PathBuf::from("/media"), PathBuf::from("/home/user/uploads"), ], write: vec![PathBuf::from("/tmp/pinakes")], }; assert_eq!(caps.read.len(), 3); assert_eq!(caps.write.len(), 1); assert!(caps.read.contains(&PathBuf::from("/data"))); assert!(caps.write.contains(&PathBuf::from("/tmp/pinakes"))); } #[tokio::test] async fn test_plugin_metadata_structure() { let metadata = PluginMetadata { id: "test-plugin".to_string(), name: "Test Plugin".to_string(), version: "1.0.0".to_string(), author: "Test Author".to_string(), description: "A test plugin for unit testing".to_string(), api_version: "1.0".to_string(), capabilities_required: Capabilities::default(), }; assert_eq!(metadata.id, "test-plugin"); assert_eq!(metadata.version, "1.0.0"); assert_eq!(metadata.api_version, "1.0"); } #[tokio::test] async fn test_network_capability_defaults() { let network = NetworkCapability::default(); assert!(!network.enabled); assert!(network.allowed_domains.is_none()); } #[tokio::test] async fn test_environment_capability_defaults() { let env = EnvironmentCapability::default(); assert!(!env.enabled); assert!(env.allowed_vars.is_none()); } #[tokio::test] async fn test_extracted_metadata_default() { let metadata = ExtractedMetadata::default(); assert!(metadata.title.is_none()); assert!(metadata.description.is_none()); assert!(metadata.author.is_none()); assert!(metadata.duration_secs.is_none()); assert!(metadata.width.is_none()); assert!(metadata.height.is_none()); assert!(metadata.custom_fields.is_empty()); assert!(metadata.tags.is_empty()); } #[tokio::test] async fn test_search_query_structure() { let query = SearchQuery { query_text: "test query".to_string(), filters: HashMap::new(), limit: 10, offset: 0, }; assert!(!query.query_text.is_empty()); assert_eq!(query.limit, 10); assert_eq!(query.offset, 0); } #[tokio::test] async fn test_thumbnail_options_structure() { let options = ThumbnailOptions { width: 640, height: 480, quality: 75, format: ThumbnailFormat::Png, }; assert_eq!(options.width, 640); assert_eq!(options.height, 480); assert_eq!(options.quality, 75); assert!(matches!(options.format, ThumbnailFormat::Png)); }