Files
readur/tests/unit_webdav_enhanced_unit_tests.rs

664 lines
23 KiB
Rust

use readur::services::webdav::{WebDAVService, WebDAVConfig, RetryConfig};
use readur::webdav_xml_parser::parse_propfind_response;
use readur::models::FileIngestionInfo;
use readur::models::source::WebDAVSyncStatus;
use readur::models::*;
use chrono::Utc;
use uuid::Uuid;
// Mock WebDAV responses for comprehensive testing
fn mock_nextcloud_propfind_response() -> String {
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/files/admin/Documents/</d:href>
<d:propstat>
<d:prop>
<d:displayname>Documents</d:displayname>
<d:getlastmodified>Tue, 01 Jan 2024 12:00:00 GMT</d:getlastmodified>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
<d:getetag>"abc123"</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/files/admin/Documents/report.pdf</d:href>
<d:propstat>
<d:prop>
<d:displayname>report.pdf</d:displayname>
<d:getcontentlength>2048000</d:getcontentlength>
<d:getlastmodified>Mon, 15 Jan 2024 14:30:00 GMT</d:getlastmodified>
<d:getcontenttype>application/pdf</d:getcontenttype>
<d:getetag>"pdf123"</d:getetag>
<d:resourcetype/>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/files/admin/Documents/photo.png</d:href>
<d:propstat>
<d:prop>
<d:displayname>photo.png</d:displayname>
<d:getcontentlength>768000</d:getcontentlength>
<d:getlastmodified>Wed, 10 Jan 2024 09:15:00 GMT</d:getlastmodified>
<d:getcontenttype>image/png</d:getcontenttype>
<d:getetag>"png456"</d:getetag>
<d:resourcetype/>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/files/admin/Documents/unsupported.docx</d:href>
<d:propstat>
<d:prop>
<d:displayname>unsupported.docx</d:displayname>
<d:getcontentlength>102400</d:getcontentlength>
<d:getlastmodified>Thu, 20 Jan 2024 16:45:00 GMT</d:getlastmodified>
<d:getcontenttype>application/vnd.openxmlformats-officedocument.wordprocessingml.document</d:getcontenttype>
<d:getetag>"docx789"</d:getetag>
<d:resourcetype/>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#.to_string()
}
fn mock_empty_folder_response() -> String {
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/webdav/EmptyFolder/</d:href>
<d:propstat>
<d:prop>
<d:displayname>EmptyFolder</d:displayname>
<d:getlastmodified>Fri, 01 Jan 2024 12:00:00 GMT</d:getlastmodified>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#.to_string()
}
fn mock_malformed_xml_response() -> String {
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/webdav/test.pdf</d:href>
<d:propstat>
<d:prop>
<d:displayname>test.pdf
<!-- Missing closing tag -->
</d:prop>
</d:propstat>
</d:response>
<!-- Incomplete XML -->"#.to_string()
}
#[test]
fn test_webdav_config_validation() {
// Test valid config
let valid_config = WebDAVConfig {
server_url: "https://cloud.example.com".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string(), "/Photos".to_string()],
file_extensions: vec!["pdf".to_string(), "png".to_string(), "jpg".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
assert!(WebDAVService::new(valid_config).is_ok());
// Test config with empty server URL - should fail with our enhanced validation
let invalid_config = WebDAVConfig {
server_url: "".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
// Should fail early with enhanced validation
assert!(WebDAVService::new(invalid_config).is_err());
// Test config with invalid URL scheme - should also fail
let invalid_scheme_config = WebDAVConfig {
server_url: "ftp://cloud.example.com".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
assert!(WebDAVService::new(invalid_scheme_config).is_err());
// Test config with relative URL - should also fail
let relative_url_config = WebDAVConfig {
server_url: "/webdav".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
assert!(WebDAVService::new(relative_url_config).is_err());
}
#[test]
fn test_webdav_url_construction_comprehensive() {
// Test Nextcloud URL construction
let nextcloud_config = WebDAVConfig {
server_url: "https://nextcloud.example.com".to_string(),
username: "admin".to_string(),
password: "secret".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
let service = WebDAVService::new(nextcloud_config).unwrap();
// URL construction is tested implicitly during service creation
// Test ownCloud URL construction
let owncloud_config = WebDAVConfig {
server_url: "https://cloud.example.com/".to_string(), // With trailing slash
username: "user123".to_string(),
password: "pass123".to_string(),
watch_folders: vec!["/Shared".to_string()],
file_extensions: vec!["jpg".to_string()],
timeout_seconds: 60,
server_type: Some("owncloud".to_string()),
};
assert!(WebDAVService::new(owncloud_config).is_ok());
// Test generic WebDAV URL construction
let generic_config = WebDAVConfig {
server_url: "https://webdav.example.com".to_string(),
username: "webdavuser".to_string(),
password: "webdavpass".to_string(),
watch_folders: vec!["/Files".to_string()],
file_extensions: vec!["txt".to_string()],
timeout_seconds: 45,
server_type: None, // No server type = generic
};
assert!(WebDAVService::new(generic_config).is_ok());
}
#[test]
fn test_webdav_response_parsing_comprehensive() {
let config = WebDAVConfig {
server_url: "https://cloud.example.com".to_string(),
username: "admin".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string(), "png".to_string(), "jpg".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
let service = WebDAVService::new(config.clone()).unwrap();
// Test Nextcloud response parsing
let nextcloud_response = mock_nextcloud_propfind_response();
let files = parse_propfind_response(&nextcloud_response);
assert!(files.is_ok());
let files = files.unwrap();
// Filter files by supported extensions
let supported_files: Vec<_> = files.iter()
.filter(|f| {
if let Some(ext) = std::path::Path::new(&f.name)
.extension()
.and_then(|e| e.to_str())
{
config.file_extensions.contains(&ext.to_lowercase())
} else {
false
}
})
.collect();
assert_eq!(supported_files.len(), 2); // Should have 2 files with supported extensions (pdf, png)
// Verify first file (report.pdf)
let pdf_file = files.iter().find(|f| f.name == "report.pdf").unwrap();
assert_eq!(pdf_file.size, 2048000);
assert_eq!(pdf_file.mime_type, "application/pdf");
assert_eq!(pdf_file.etag, "pdf123"); // ETag should be normalized (quotes removed)
assert!(!pdf_file.is_directory);
// Verify second file (photo.png)
let png_file = files.iter().find(|f| f.name == "photo.png").unwrap();
assert_eq!(png_file.size, 768000);
assert_eq!(png_file.mime_type, "image/png");
assert_eq!(png_file.etag, "png456"); // ETag should be normalized (quotes removed)
assert!(!png_file.is_directory);
// Verify that unsupported file (docx) is not included in supported files
assert!(supported_files.iter().find(|f| f.name == "unsupported.docx").is_none());
}
#[test]
fn test_empty_folder_parsing() {
let config = WebDAVConfig {
server_url: "https://cloud.example.com".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/EmptyFolder".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("generic".to_string()),
};
let service = WebDAVService::new(config).unwrap();
let response = mock_empty_folder_response();
let files = parse_propfind_response(&response);
assert!(files.is_ok());
let files = files.unwrap();
assert_eq!(files.len(), 0); // Empty folder should have no files
}
#[test]
fn test_malformed_xml_handling() {
let config = WebDAVConfig {
server_url: "https://cloud.example.com".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
let service = WebDAVService::new(config).unwrap();
let response = mock_malformed_xml_response();
// Current simple parser might still extract some data from malformed XML
let result = parse_propfind_response(&response);
// It might succeed or fail depending on how robust the parser is
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_retry_config_custom_values() {
let custom_retry = RetryConfig {
max_retries: 5,
initial_delay_ms: 500,
max_delay_ms: 15000,
backoff_multiplier: 1.5,
timeout_seconds: 90,
rate_limit_backoff_ms: 10000,
};
assert_eq!(custom_retry.max_retries, 5);
assert_eq!(custom_retry.initial_delay_ms, 500);
assert_eq!(custom_retry.max_delay_ms, 15000);
assert_eq!(custom_retry.backoff_multiplier, 1.5);
assert_eq!(custom_retry.timeout_seconds, 90);
assert_eq!(custom_retry.rate_limit_backoff_ms, 10000);
let config = WebDAVConfig {
server_url: "https://cloud.example.com".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string()],
timeout_seconds: 30,
server_type: Some("nextcloud".to_string()),
};
assert!(WebDAVService::new_with_retry(config, custom_retry).is_ok());
}
#[test]
fn test_file_extension_matching() {
let supported_extensions = vec!["pdf", "png", "jpg", "jpeg", "tiff", "bmp", "txt"];
let test_cases = vec![
("document.pdf", true),
("image.PNG", true), // Case insensitive
("photo.jpg", true),
("photo.JPEG", true),
("scan.tiff", true),
("bitmap.bmp", true),
("readme.txt", true),
("spreadsheet.xlsx", false),
("presentation.pptx", false),
("archive.zip", false),
("script.sh", false),
("no_extension", false),
(".hidden", false),
];
for (filename, should_match) in test_cases {
let extension = std::path::Path::new(filename)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase());
let matches = extension
.as_ref()
.map(|ext| supported_extensions.contains(&ext.as_str()))
.unwrap_or(false);
assert_eq!(matches, should_match,
"File '{}' extension matching failed. Expected: {}, Got: {}",
filename, should_match, matches);
}
}
#[test]
fn test_webdav_sync_state_model() {
let sync_state = WebDAVSyncState {
id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
last_sync_at: Some(Utc::now()),
sync_cursor: Some("cursor123".to_string()),
is_running: true,
files_processed: 42,
files_remaining: 58,
current_folder: Some("/Documents".to_string()),
errors: vec!["Error 1".to_string(), "Error 2".to_string()],
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(sync_state.is_running);
assert_eq!(sync_state.files_processed, 42);
assert_eq!(sync_state.files_remaining, 58);
assert_eq!(sync_state.current_folder, Some("/Documents".to_string()));
assert_eq!(sync_state.errors.len(), 2);
}
#[test]
fn test_webdav_file_model() {
let document_id = Uuid::new_v4();
let webdav_file = WebDAVFile {
id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
webdav_path: "/Documents/report.pdf".to_string(),
etag: "\"abc123\"".to_string(),
last_modified: Some(Utc::now()),
file_size: 2048000,
mime_type: "application/pdf".to_string(),
document_id: Some(document_id),
sync_status: "completed".to_string(),
sync_error: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert_eq!(webdav_file.webdav_path, "/Documents/report.pdf");
assert_eq!(webdav_file.etag, "\"abc123\"");
assert_eq!(webdav_file.file_size, 2048000);
assert_eq!(webdav_file.sync_status, "completed");
assert!(webdav_file.sync_error.is_none());
}
#[test]
fn test_create_webdav_file_model() {
let user_id = Uuid::new_v4();
let create_file = CreateWebDAVFile {
user_id,
webdav_path: "/Photos/vacation.jpg".to_string(),
etag: "\"photo123\"".to_string(),
last_modified: Some(Utc::now()),
file_size: 1536000,
mime_type: "image/jpeg".to_string(),
document_id: None,
sync_status: "pending".to_string(),
sync_error: None,
};
assert_eq!(create_file.user_id, user_id);
assert_eq!(create_file.webdav_path, "/Photos/vacation.jpg");
assert_eq!(create_file.file_size, 1536000);
assert_eq!(create_file.sync_status, "pending");
}
#[test]
fn test_update_webdav_sync_state_model() {
let update_state = UpdateWebDAVSyncState {
last_sync_at: Some(Utc::now()),
sync_cursor: Some("new_cursor".to_string()),
is_running: false,
files_processed: 100,
files_remaining: 0,
current_folder: None,
errors: Vec::new(),
};
assert!(!update_state.is_running);
assert_eq!(update_state.files_processed, 100);
assert_eq!(update_state.files_remaining, 0);
assert!(update_state.current_folder.is_none());
assert!(update_state.errors.is_empty());
}
#[test]
fn test_ocr_priority_calculation_comprehensive() {
let test_cases = vec![
// Size boundaries
(0, 10), // 0 bytes
(1, 10), // 1 byte
(1048576, 10), // Exactly 1MB
(1048577, 8), // 1MB + 1 byte
(5242880, 8), // Exactly 5MB
(5242881, 6), // 5MB + 1 byte
(10485760, 6), // Exactly 10MB
(10485761, 4), // 10MB + 1 byte
(52428800, 4), // Exactly 50MB
(52428801, 2), // 50MB + 1 byte
(104857600, 2), // 100MB
(1073741824, 2), // 1GB
];
for (file_size, expected_priority) in test_cases {
let priority = match file_size {
0..=1048576 => 10, // <= 1MB
..=5242880 => 8, // 1-5MB
..=10485760 => 6, // 5-10MB
..=52428800 => 4, // 10-50MB
_ => 2, // > 50MB
};
assert_eq!(priority, expected_priority,
"Priority calculation failed for file size {} bytes", file_size);
}
}
#[test]
fn test_sync_status_serialization() {
let sync_status = WebDAVSyncStatus {
is_running: true,
last_sync: Some(Utc::now()),
files_processed: 25,
files_remaining: 75,
current_folder: Some("/Documents/Reports".to_string()),
errors: vec!["Connection timeout".to_string()],
};
// Test that the status can be serialized to JSON
let json = serde_json::to_string(&sync_status);
assert!(json.is_ok());
let json_str = json.unwrap();
assert!(json_str.contains("\"is_running\":true"));
assert!(json_str.contains("\"files_processed\":25"));
assert!(json_str.contains("\"files_remaining\":75"));
assert!(json_str.contains("\"current_folder\":\"/Documents/Reports\""));
}
#[test]
fn test_crawl_estimate_calculation() {
let folder1 = WebDAVFolderInfo {
path: "/Documents".to_string(),
total_files: 100,
supported_files: 80,
estimated_time_hours: 0.044, // ~2.6 minutes
total_size_mb: 150.0,
};
let folder2 = WebDAVFolderInfo {
path: "/Photos".to_string(),
total_files: 200,
supported_files: 150,
estimated_time_hours: 0.083, // ~5 minutes
total_size_mb: 500.0,
};
let estimate = WebDAVCrawlEstimate {
folders: vec![folder1, folder2],
total_files: 300,
total_supported_files: 230,
total_estimated_time_hours: 0.127, // ~7.6 minutes
total_size_mb: 650.0,
};
assert_eq!(estimate.folders.len(), 2);
assert_eq!(estimate.total_files, 300);
assert_eq!(estimate.total_supported_files, 230);
assert!((estimate.total_estimated_time_hours - 0.127).abs() < 0.001);
assert_eq!(estimate.total_size_mb, 650.0);
}
#[test]
fn test_connection_result_variants() {
// Success case
let success_result = WebDAVConnectionResult {
success: true,
message: "Connected successfully to Nextcloud 28.0.1".to_string(),
server_version: Some("28.0.1".to_string()),
server_type: Some("nextcloud".to_string()),
};
assert!(success_result.success);
assert!(success_result.server_version.is_some());
assert_eq!(success_result.server_type, Some("nextcloud".to_string()));
// Failure case
let failure_result = WebDAVConnectionResult {
success: false,
message: "Authentication failed: 401 Unauthorized".to_string(),
server_version: None,
server_type: None,
};
assert!(!failure_result.success);
assert!(failure_result.server_version.is_none());
assert!(failure_result.server_type.is_none());
assert!(failure_result.message.contains("401"));
}
#[test]
fn test_notification_creation_for_webdav() {
let notification = CreateNotification {
notification_type: "info".to_string(),
title: "WebDAV Sync Started".to_string(),
message: "Synchronizing files from Nextcloud server".to_string(),
action_url: Some("/sync-status".to_string()),
metadata: Some(serde_json::json!({
"sync_type": "webdav",
"folders": ["/Documents", "/Photos"],
"estimated_files": 150
})),
};
assert_eq!(notification.notification_type, "info");
assert_eq!(notification.title, "WebDAV Sync Started");
assert!(notification.action_url.is_some());
let metadata = notification.metadata.unwrap();
assert_eq!(metadata["sync_type"], "webdav");
assert!(metadata["folders"].is_array());
assert_eq!(metadata["estimated_files"], 150);
}
#[test]
fn test_special_characters_in_paths() {
let test_paths = vec![
"/Documents/File with spaces.pdf",
"/Documents/Ñoño/archivo.pdf",
"/Documents/测试文件.pdf",
"/Documents/файл.pdf",
"/Documents/50%.pdf",
"/Documents/file&name.pdf",
"/Documents/file#1.pdf",
];
for path in test_paths {
let file_info = FileIngestionInfo {
relative_path: path.to_string(),
full_path: path.to_string(),
#[allow(deprecated)]
path: path.to_string(),
name: std::path::Path::new(path)
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string(),
size: 1024,
mime_type: "application/pdf".to_string(),
last_modified: Some(Utc::now()),
etag: "\"test123\"".to_string(),
is_directory: false,
created_at: None,
permissions: None,
owner: None,
group: None,
metadata: None,
};
assert!(!file_info.name.is_empty());
assert!(file_info.name.ends_with(".pdf"));
}
}
#[test]
fn test_backoff_delay_calculation() {
let retry_config = RetryConfig::default();
let mut delays = Vec::new();
let mut delay = retry_config.initial_delay_ms;
for _ in 0..5 {
delays.push(delay);
delay = ((delay as f64 * retry_config.backoff_multiplier) as u64)
.min(retry_config.max_delay_ms);
}
assert_eq!(delays[0], 1000); // 1s
assert_eq!(delays[1], 2000); // 2s
assert_eq!(delays[2], 4000); // 4s
assert_eq!(delays[3], 8000); // 8s
assert_eq!(delays[4], 10000); // 10s (capped at max_delay_ms)
// Verify max delay is respected
for _ in 0..10 {
delay = ((delay as f64 * retry_config.backoff_multiplier) as u64)
.min(retry_config.max_delay_ms);
}
assert_eq!(delay, retry_config.max_delay_ms);
}