Skip to main content

max / goingson

14.0 KB · 379 lines History Blame Raw
1 //! Integration tests for export and backup commands.
2 //!
3 //! Since export commands require Tauri `State<'_>` and `AppHandle` which cannot
4 //! be constructed in tests, we test through the repository layer (data retrieval)
5 //! and the export utility layer (file writing), matching the patterns in
6 //! `src/export/backup.rs`, `csv.rs`, and `ics.rs`.
7
8 use chrono::{Duration, Utc};
9 use tempfile::tempdir;
10
11 use goingson_core::{NewEvent, NewTask, Priority};
12
13 use crate::export::{backup, csv, ics};
14 use crate::test_utils::{create_test_project, setup_test_state};
15
16 // ============ JSON Export ============
17
18 #[tokio::test]
19 async fn test_export_json_success() {
20 let (state, user_id) = setup_test_state().await;
21 let project_id = create_test_project(&state, user_id).await;
22
23 // Seed data
24 let task = NewTask::builder("Write report")
25 .project_id(project_id)
26 .priority(Priority::High)
27 .build();
28 state.tasks.create(user_id, task).await.unwrap();
29
30 let start = Utc::now() + Duration::days(1);
31 let event = NewEvent::builder("Sprint planning", start)
32 .project_id(project_id)
33 .build();
34 state.events.create(user_id, event).await.unwrap();
35
36 // Fetch all data (mirrors what export_json does)
37 let projects = state.projects.list_all(user_id).await.unwrap();
38 let tasks = state.tasks.list_all(user_id).await.unwrap();
39 let events = state.events.list_all(user_id).await.unwrap();
40 let emails = state.emails.list_all(user_id, true).await.unwrap();
41
42 assert_eq!(projects.len(), 1);
43 assert_eq!(tasks.len(), 1);
44 assert_eq!(events.len(), 1);
45 assert_eq!(emails.len(), 0);
46
47 // Write JSON export
48 let export = backup::FullExport::new(projects, tasks, events, emails, vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![]);
49 assert_eq!(export.total_count(), 3); // 1 project + 1 task + 1 event
50
51 let dir = tempdir().unwrap();
52 let json_path = dir.path().join("export.json");
53 let size = backup::write_json(&export, &json_path).unwrap();
54 assert!(size > 0);
55
56 // Verify JSON content is readable
57 let content = std::fs::read_to_string(&json_path).unwrap();
58 assert!(content.contains("Write report"));
59 assert!(content.contains("Sprint planning"));
60 assert!(content.contains("Test Project"));
61 }
62
63 #[tokio::test]
64 async fn test_export_json_empty_database() {
65 let (state, user_id) = setup_test_state().await;
66
67 let projects = state.projects.list_all(user_id).await.unwrap();
68 let tasks = state.tasks.list_all(user_id).await.unwrap();
69 let events = state.events.list_all(user_id).await.unwrap();
70 let emails = state.emails.list_all(user_id, true).await.unwrap();
71
72 let export = backup::FullExport::new(projects, tasks, events, emails, vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![]);
73 assert_eq!(export.total_count(), 0);
74
75 let dir = tempdir().unwrap();
76 let json_path = dir.path().join("empty-export.json");
77 let size = backup::write_json(&export, &json_path).unwrap();
78 assert!(size > 0);
79 }
80
81 // ============ CSV Export ============
82
83 #[tokio::test]
84 async fn test_export_tasks_csv_success() {
85 let (state, user_id) = setup_test_state().await;
86 let project_id = create_test_project(&state, user_id).await;
87
88 // Create tasks with varying attributes
89 let task1 = NewTask::builder("Task with project")
90 .project_id(project_id)
91 .priority(Priority::High)
92 .tag("important")
93 .build();
94 state.tasks.create(user_id, task1).await.unwrap();
95
96 let task2 = NewTask::builder("Standalone task")
97 .priority(Priority::Low)
98 .build();
99 state.tasks.create(user_id, task2).await.unwrap();
100
101 let tasks = state.tasks.list_all(user_id).await.unwrap();
102 let projects = state.projects.list_all(user_id).await.unwrap();
103
104 assert_eq!(tasks.len(), 2);
105
106 let mut buffer = Vec::new();
107 let count = csv::write_tasks_csv(&tasks, &projects, &mut buffer).unwrap();
108 assert_eq!(count, 2);
109
110 let output = String::from_utf8(buffer).unwrap();
111 assert!(output.contains("Task with project"));
112 assert!(output.contains("Standalone task"));
113 assert!(output.contains("important"));
114 assert!(output.contains("Test Project"));
115 }
116
117 #[tokio::test]
118 async fn test_export_tasks_csv_filtered_by_project() {
119 let (state, user_id) = setup_test_state().await;
120 let project_id = create_test_project(&state, user_id).await;
121
122 // One task in the project, one without
123 let in_project = NewTask::builder("In project")
124 .project_id(project_id)
125 .priority(Priority::Medium)
126 .build();
127 state.tasks.create(user_id, in_project).await.unwrap();
128
129 let standalone = NewTask::builder("Not in project")
130 .priority(Priority::Medium)
131 .build();
132 state.tasks.create(user_id, standalone).await.unwrap();
133
134 // Filter by project (mirrors what export_tasks_csv does with project_id)
135 let filtered = state.tasks.list_by_project(user_id, project_id).await.unwrap();
136 assert_eq!(filtered.len(), 1);
137 assert_eq!(filtered[0].description, "In project");
138
139 let projects = state.projects.list_all(user_id).await.unwrap();
140 let mut buffer = Vec::new();
141 let count = csv::write_tasks_csv(&filtered, &projects, &mut buffer).unwrap();
142 assert_eq!(count, 1);
143 }
144
145 // ============ ICS Export ============
146
147 #[tokio::test]
148 async fn test_export_events_ics_success() {
149 let (state, user_id) = setup_test_state().await;
150
151 let future_start = Utc::now() + Duration::days(3);
152 let event = NewEvent::builder("Team standup", future_start)
153 .end_time(future_start + Duration::hours(1))
154 .location("Room B")
155 .build();
156 state.events.create(user_id, event).await.unwrap();
157
158 let events = state.events.list_all(user_id).await.unwrap();
159 assert_eq!(events.len(), 1);
160
161 let mut buffer = Vec::new();
162 let count = ics::write_events_ics(&events, true, &mut buffer).unwrap();
163 assert_eq!(count, 1);
164
165 let output = String::from_utf8(buffer).unwrap();
166 assert!(output.contains("BEGIN:VCALENDAR"));
167 assert!(output.contains("Team standup"));
168 assert!(output.contains("Room B"));
169 assert!(output.contains("END:VCALENDAR"));
170 }
171
172 #[tokio::test]
173 async fn test_export_events_ics_exclude_past() {
174 let (state, user_id) = setup_test_state().await;
175
176 // Create a past event
177 let past_start = Utc::now() - Duration::days(7);
178 let past_event = NewEvent::builder("Past meeting", past_start)
179 .end_time(past_start + Duration::hours(1))
180 .build();
181 state.events.create(user_id, past_event).await.unwrap();
182
183 // Create a future event
184 let future_start = Utc::now() + Duration::days(7);
185 let future_event = NewEvent::builder("Future meeting", future_start)
186 .end_time(future_start + Duration::hours(1))
187 .build();
188 state.events.create(user_id, future_event).await.unwrap();
189
190 let events = state.events.list_all(user_id).await.unwrap();
191 assert_eq!(events.len(), 2);
192
193 // Exclude past events
194 let mut buffer = Vec::new();
195 let count = ics::write_events_ics(&events, false, &mut buffer).unwrap();
196 assert_eq!(count, 1);
197
198 let output = String::from_utf8(buffer).unwrap();
199 assert!(output.contains("Future meeting"));
200 assert!(!output.contains("Past meeting"));
201 }
202
203 // ============ Backup ============
204
205 #[tokio::test]
206 async fn test_create_backup_success() {
207 let (state, user_id) = setup_test_state().await;
208 let project_id = create_test_project(&state, user_id).await;
209
210 let task = NewTask::builder("Backup me")
211 .project_id(project_id)
212 .priority(Priority::Medium)
213 .build();
214 state.tasks.create(user_id, task).await.unwrap();
215
216 let start = Utc::now() + Duration::days(2);
217 let event = NewEvent::builder("Backed up event", start).build();
218 state.events.create(user_id, event).await.unwrap();
219
220 // Fetch all data and create backup (mirrors create_backup command)
221 let projects = state.projects.list_all(user_id).await.unwrap();
222 let tasks = state.tasks.list_all(user_id).await.unwrap();
223 let events = state.events.list_all(user_id).await.unwrap();
224 let emails = state.emails.list_all(user_id, true).await.unwrap();
225
226 let export = backup::FullExport::new(projects, tasks, events, emails, vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![]);
227 assert_eq!(export.total_count(), 3);
228
229 let dir = tempdir().unwrap();
230 let backup_path = dir.path().join("goingson-backup-test.json.gz");
231 let size = backup::write_backup(&export, &backup_path).unwrap();
232 assert!(size > 0);
233
234 // Verify round-trip
235 let restored = backup::read_backup(&backup_path).unwrap();
236 assert_eq!(restored.total_count(), 3);
237 assert_eq!(restored.projects.len(), 1);
238 assert_eq!(restored.tasks.len(), 1);
239 assert_eq!(restored.tasks[0].description, "Backup me");
240 assert_eq!(restored.events.len(), 1);
241 assert_eq!(restored.events[0].title, "Backed up event");
242 }
243
244 #[tokio::test]
245 async fn test_backup_read_nonexistent_file() {
246 let result = backup::read_backup("/tmp/nonexistent-backup-file.json.gz");
247 assert!(result.is_err());
248 }
249
250 // ============ Path Validation ============
251
252 #[test]
253 fn test_validate_export_path_rejects_traversal() {
254 use crate::commands::export::validate_export_path;
255
256 let result = validate_export_path("/tmp/../etc/passwd");
257 assert!(result.is_err());
258 }
259
260 #[test]
261 fn test_validate_export_path_accepts_normal_path() {
262 use crate::commands::export::validate_export_path;
263
264 let result = validate_export_path("/tmp/goingson/export.json");
265 assert!(result.is_ok());
266 }
267
268 // ============ Export Summary ============
269
270 // ============ Backup completeness (CHRONIC-D) ============
271
272 #[tokio::test]
273 async fn test_collect_full_export_includes_saved_views_and_reviews() {
274 use chrono::NaiveDate;
275 use goingson_core::models::{NewSavedView, ViewFilters, ViewType};
276
277 let (state, user_id) = setup_test_state().await;
278
279 // Seed one row in each previously-omitted collection (ultra-fuzz Run #27 SERIOUS:
280 // backup dropped saved views, weekly/monthly reviews, vacation days).
281 state
282 .saved_views
283 .create(
284 user_id,
285 NewSavedView {
286 name: "Pinned tasks".into(),
287 view_type: ViewType::Tasks,
288 filters: ViewFilters::default(),
289 sort_by: None,
290 sort_order: None,
291 is_pinned: Some(true),
292 },
293 )
294 .await
295 .unwrap();
296
297 let week_start = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
298 state.weekly_reviews.upsert(user_id, week_start, "Good week").await.unwrap();
299 state.weekly_reviews.set_vacation_days(user_id, week_start, &[0, 4]).await.unwrap();
300
301 state.monthly_reviews.upsert_goal(user_id, "2026-06", "Ship v1", 1).await.unwrap();
302 state.monthly_reviews.upsert_reflection(user_id, "2026-06", "Highlight", "Change").await.unwrap();
303
304 // The single backup-gather path must now carry all four.
305 let export = crate::backup_scheduler::collect_full_export(&state, user_id).await.unwrap();
306 assert_eq!(export.saved_views.len(), 1, "saved views gathered");
307 assert_eq!(export.weekly_reviews.len(), 1, "weekly reviews gathered");
308 assert_eq!(export.weekly_reviews[0].vacation_days, vec![0, 4], "vacation days carried");
309 assert_eq!(export.monthly_goals.len(), 1, "monthly goals gathered");
310 assert_eq!(export.monthly_reflections.len(), 1, "monthly reflections gathered");
311
312 // And they survive a write/read serialization round-trip.
313 let dir = tempdir().unwrap();
314 let path = dir.path().join("backup.json.gz");
315 backup::write_backup(&export, &path).unwrap();
316 let restored = backup::read_backup(&path).unwrap();
317 assert_eq!(restored.saved_views.len(), 1);
318 assert_eq!(restored.weekly_reviews.len(), 1);
319 assert_eq!(restored.monthly_goals.len(), 1);
320 assert_eq!(restored.monthly_reflections.len(), 1);
321
322 // And restore writes them into a fresh database (exercises the restore_* fns).
323 let (dst, dst_user) = setup_test_state().await;
324 let input = goingson_core::backup_restore::RestoreInput {
325 projects: restored.projects,
326 tasks: restored.tasks,
327 events: restored.events,
328 emails: restored.emails,
329 contacts: restored.contacts,
330 time_sessions: restored.time_sessions,
331 milestones: restored.milestones,
332 daily_notes: restored.daily_notes,
333 attachments: restored.attachments,
334 sync_accounts: restored.sync_accounts,
335 saved_views: restored.saved_views,
336 weekly_reviews: restored.weekly_reviews,
337 monthly_goals: restored.monthly_goals,
338 monthly_reflections: restored.monthly_reflections,
339 };
340 let result = goingson_db_sqlite::restore_all(&dst.pool, dst_user, &input).await.unwrap();
341 assert_eq!(result.saved_views_restored, 1);
342 assert_eq!(result.weekly_reviews_restored, 1);
343 assert_eq!(result.monthly_goals_restored, 1);
344 assert_eq!(result.monthly_reflections_restored, 1);
345 assert_eq!(dst.saved_views.list_all(dst_user).await.unwrap().len(), 1);
346 assert_eq!(dst.weekly_reviews.list_all(dst_user).await.unwrap().len(), 1);
347 }
348
349 #[tokio::test]
350 async fn test_export_summary_counts() {
351 let (state, user_id) = setup_test_state().await;
352 let project_id = create_test_project(&state, user_id).await;
353
354 // Create 2 tasks
355 for desc in &["Task A", "Task B"] {
356 let task = NewTask::builder(*desc)
357 .project_id(project_id)
358 .priority(Priority::Medium)
359 .build();
360 state.tasks.create(user_id, task).await.unwrap();
361 }
362
363 // Create 1 event
364 let start = Utc::now() + Duration::days(1);
365 let event = NewEvent::builder("Summary event", start).build();
366 state.events.create(user_id, event).await.unwrap();
367
368 // Mirrors get_export_summary
369 let projects = state.projects.list_all(user_id).await.unwrap();
370 let tasks = state.tasks.list_all(user_id).await.unwrap();
371 let events = state.events.list_all(user_id).await.unwrap();
372 let emails = state.emails.list_all(user_id, true).await.unwrap();
373
374 assert_eq!(projects.len(), 1);
375 assert_eq!(tasks.len(), 2);
376 assert_eq!(events.len(), 1);
377 assert_eq!(emails.len(), 0);
378 }
379