//! Integration tests for export and backup commands. //! //! Since export commands require Tauri `State<'_>` and `AppHandle` which cannot //! be constructed in tests, we test through the repository layer (data retrieval) //! and the export utility layer (file writing), matching the patterns in //! `src/export/backup.rs`, `csv.rs`, and `ics.rs`. use chrono::{Duration, Utc}; use tempfile::tempdir; use goingson_core::{NewEvent, NewTask, Priority}; use crate::export::{backup, csv, ics}; use crate::test_utils::{create_test_project, setup_test_state}; // ============ JSON Export ============ #[tokio::test] async fn test_export_json_success() { let (state, user_id) = setup_test_state().await; let project_id = create_test_project(&state, user_id).await; // Seed data let task = NewTask::builder("Write report") .project_id(project_id) .priority(Priority::High) .build(); state.tasks.create(user_id, task).await.unwrap(); let start = Utc::now() + Duration::days(1); let event = NewEvent::builder("Sprint planning", start) .project_id(project_id) .build(); state.events.create(user_id, event).await.unwrap(); // Fetch all data (mirrors what export_json does) let projects = state.projects.list_all(user_id).await.unwrap(); let tasks = state.tasks.list_all(user_id).await.unwrap(); let events = state.events.list_all(user_id).await.unwrap(); let emails = state.emails.list_all(user_id, true).await.unwrap(); assert_eq!(projects.len(), 1); assert_eq!(tasks.len(), 1); assert_eq!(events.len(), 1); assert_eq!(emails.len(), 0); // Write JSON export let export = backup::FullExport::new(projects, tasks, events, emails, vec![]); assert_eq!(export.total_count(), 3); // 1 project + 1 task + 1 event let dir = tempdir().unwrap(); let json_path = dir.path().join("export.json"); let size = backup::write_json(&export, &json_path).unwrap(); assert!(size > 0); // Verify JSON content is readable let content = std::fs::read_to_string(&json_path).unwrap(); assert!(content.contains("Write report")); assert!(content.contains("Sprint planning")); assert!(content.contains("Test Project")); } #[tokio::test] async fn test_export_json_empty_database() { let (state, user_id) = setup_test_state().await; let projects = state.projects.list_all(user_id).await.unwrap(); let tasks = state.tasks.list_all(user_id).await.unwrap(); let events = state.events.list_all(user_id).await.unwrap(); let emails = state.emails.list_all(user_id, true).await.unwrap(); let export = backup::FullExport::new(projects, tasks, events, emails, vec![]); assert_eq!(export.total_count(), 0); let dir = tempdir().unwrap(); let json_path = dir.path().join("empty-export.json"); let size = backup::write_json(&export, &json_path).unwrap(); assert!(size > 0); } // ============ CSV Export ============ #[tokio::test] async fn test_export_tasks_csv_success() { let (state, user_id) = setup_test_state().await; let project_id = create_test_project(&state, user_id).await; // Create tasks with varying attributes let task1 = NewTask::builder("Task with project") .project_id(project_id) .priority(Priority::High) .tag("important") .build(); state.tasks.create(user_id, task1).await.unwrap(); let task2 = NewTask::builder("Standalone task") .priority(Priority::Low) .build(); state.tasks.create(user_id, task2).await.unwrap(); let tasks = state.tasks.list_all(user_id).await.unwrap(); let projects = state.projects.list_all(user_id).await.unwrap(); assert_eq!(tasks.len(), 2); let mut buffer = Vec::new(); let count = csv::write_tasks_csv(&tasks, &projects, &mut buffer).unwrap(); assert_eq!(count, 2); let output = String::from_utf8(buffer).unwrap(); assert!(output.contains("Task with project")); assert!(output.contains("Standalone task")); assert!(output.contains("important")); assert!(output.contains("Test Project")); } #[tokio::test] async fn test_export_tasks_csv_filtered_by_project() { let (state, user_id) = setup_test_state().await; let project_id = create_test_project(&state, user_id).await; // One task in the project, one without let in_project = NewTask::builder("In project") .project_id(project_id) .priority(Priority::Medium) .build(); state.tasks.create(user_id, in_project).await.unwrap(); let standalone = NewTask::builder("Not in project") .priority(Priority::Medium) .build(); state.tasks.create(user_id, standalone).await.unwrap(); // Filter by project (mirrors what export_tasks_csv does with project_id) let filtered = state.tasks.list_by_project(user_id, project_id).await.unwrap(); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].description, "In project"); let projects = state.projects.list_all(user_id).await.unwrap(); let mut buffer = Vec::new(); let count = csv::write_tasks_csv(&filtered, &projects, &mut buffer).unwrap(); assert_eq!(count, 1); } // ============ ICS Export ============ #[tokio::test] async fn test_export_events_ics_success() { let (state, user_id) = setup_test_state().await; let future_start = Utc::now() + Duration::days(3); let event = NewEvent::builder("Team standup", future_start) .end_time(future_start + Duration::hours(1)) .location("Room B") .build(); state.events.create(user_id, event).await.unwrap(); let events = state.events.list_all(user_id).await.unwrap(); assert_eq!(events.len(), 1); let mut buffer = Vec::new(); let count = ics::write_events_ics(&events, true, &mut buffer).unwrap(); assert_eq!(count, 1); let output = String::from_utf8(buffer).unwrap(); assert!(output.contains("BEGIN:VCALENDAR")); assert!(output.contains("Team standup")); assert!(output.contains("Room B")); assert!(output.contains("END:VCALENDAR")); } #[tokio::test] async fn test_export_events_ics_exclude_past() { let (state, user_id) = setup_test_state().await; // Create a past event let past_start = Utc::now() - Duration::days(7); let past_event = NewEvent::builder("Past meeting", past_start) .end_time(past_start + Duration::hours(1)) .build(); state.events.create(user_id, past_event).await.unwrap(); // Create a future event let future_start = Utc::now() + Duration::days(7); let future_event = NewEvent::builder("Future meeting", future_start) .end_time(future_start + Duration::hours(1)) .build(); state.events.create(user_id, future_event).await.unwrap(); let events = state.events.list_all(user_id).await.unwrap(); assert_eq!(events.len(), 2); // Exclude past events let mut buffer = Vec::new(); let count = ics::write_events_ics(&events, false, &mut buffer).unwrap(); assert_eq!(count, 1); let output = String::from_utf8(buffer).unwrap(); assert!(output.contains("Future meeting")); assert!(!output.contains("Past meeting")); } // ============ Backup ============ #[tokio::test] async fn test_create_backup_success() { let (state, user_id) = setup_test_state().await; let project_id = create_test_project(&state, user_id).await; let task = NewTask::builder("Backup me") .project_id(project_id) .priority(Priority::Medium) .build(); state.tasks.create(user_id, task).await.unwrap(); let start = Utc::now() + Duration::days(2); let event = NewEvent::builder("Backed up event", start).build(); state.events.create(user_id, event).await.unwrap(); // Fetch all data and create backup (mirrors create_backup command) let projects = state.projects.list_all(user_id).await.unwrap(); let tasks = state.tasks.list_all(user_id).await.unwrap(); let events = state.events.list_all(user_id).await.unwrap(); let emails = state.emails.list_all(user_id, true).await.unwrap(); let export = backup::FullExport::new(projects, tasks, events, emails, vec![]); assert_eq!(export.total_count(), 3); let dir = tempdir().unwrap(); let backup_path = dir.path().join("goingson-backup-test.json.gz"); let size = backup::write_backup(&export, &backup_path).unwrap(); assert!(size > 0); // Verify round-trip let restored = backup::read_backup(&backup_path).unwrap(); assert_eq!(restored.total_count(), 3); assert_eq!(restored.projects.len(), 1); assert_eq!(restored.tasks.len(), 1); assert_eq!(restored.tasks[0].description, "Backup me"); assert_eq!(restored.events.len(), 1); assert_eq!(restored.events[0].title, "Backed up event"); } #[tokio::test] async fn test_backup_read_nonexistent_file() { let result = backup::read_backup("/tmp/nonexistent-backup-file.json.gz"); assert!(result.is_err()); } // ============ Path Validation ============ #[test] fn test_validate_export_path_rejects_traversal() { use crate::commands::export::validate_export_path; let result = validate_export_path("/tmp/../etc/passwd"); assert!(result.is_err()); } #[test] fn test_validate_export_path_accepts_normal_path() { use crate::commands::export::validate_export_path; let result = validate_export_path("/tmp/goingson/export.json"); assert!(result.is_ok()); } // ============ Export Summary ============ #[tokio::test] async fn test_export_summary_counts() { let (state, user_id) = setup_test_state().await; let project_id = create_test_project(&state, user_id).await; // Create 2 tasks for desc in &["Task A", "Task B"] { let task = NewTask::builder(*desc) .project_id(project_id) .priority(Priority::Medium) .build(); state.tasks.create(user_id, task).await.unwrap(); } // Create 1 event let start = Utc::now() + Duration::days(1); let event = NewEvent::builder("Summary event", start).build(); state.events.create(user_id, event).await.unwrap(); // Mirrors get_export_summary let projects = state.projects.list_all(user_id).await.unwrap(); let tasks = state.tasks.list_all(user_id).await.unwrap(); let events = state.events.list_all(user_id).await.unwrap(); let emails = state.emails.list_all(user_id, true).await.unwrap(); assert_eq!(projects.len(), 1); assert_eq!(tasks.len(), 2); assert_eq!(events.len(), 1); assert_eq!(emails.len(), 0); }