Skip to main content

max / goingson

10.3 KB · 300 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![]);
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![]);
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![]);
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 #[tokio::test]
271 async fn test_export_summary_counts() {
272 let (state, user_id) = setup_test_state().await;
273 let project_id = create_test_project(&state, user_id).await;
274
275 // Create 2 tasks
276 for desc in &["Task A", "Task B"] {
277 let task = NewTask::builder(*desc)
278 .project_id(project_id)
279 .priority(Priority::Medium)
280 .build();
281 state.tasks.create(user_id, task).await.unwrap();
282 }
283
284 // Create 1 event
285 let start = Utc::now() + Duration::days(1);
286 let event = NewEvent::builder("Summary event", start).build();
287 state.events.create(user_id, event).await.unwrap();
288
289 // Mirrors get_export_summary
290 let projects = state.projects.list_all(user_id).await.unwrap();
291 let tasks = state.tasks.list_all(user_id).await.unwrap();
292 let events = state.events.list_all(user_id).await.unwrap();
293 let emails = state.emails.list_all(user_id, true).await.unwrap();
294
295 assert_eq!(projects.len(), 1);
296 assert_eq!(tasks.len(), 2);
297 assert_eq!(events.len(), 1);
298 assert_eq!(emails.len(), 0);
299 }
300