Skip to main content

max / goingson

13.0 KB · 437 lines History Blame Raw
1 //! Integration tests for SqliteMilestoneRepository.
2
3 mod common;
4
5 use goingson_core::{
6 MilestoneRepository, MilestoneStatus, NewMilestone, NewProject, ProjectRepository,
7 ProjectStatus, ProjectType,
8 };
9 use goingson_db_sqlite::{SqliteMilestoneRepository, SqliteProjectRepository};
10
11 #[tokio::test]
12 async fn test_create_and_get_milestone() {
13 let pool = common::setup_test_db().await;
14 let user_id = common::create_test_user(&pool).await;
15 let project_repo = SqliteProjectRepository::new(pool.clone());
16 let project = project_repo
17 .create(
18 user_id,
19 NewProject {
20 name: "Test Project".to_string(),
21 description: String::new(),
22 project_type: ProjectType::SideProject,
23 status: ProjectStatus::Active,
24 },
25 )
26 .await
27 .unwrap();
28 let repo = SqliteMilestoneRepository::new(pool);
29
30 let target = chrono::NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
31 let created = repo
32 .create(
33 user_id,
34 NewMilestone {
35 project_id: project.id,
36 name: "Alpha Release".to_string(),
37 description: "First public release".to_string(),
38 position: 0,
39 target_date: Some(target),
40 },
41 )
42 .await
43 .expect("Failed to create milestone");
44
45 assert_eq!(created.name, "Alpha Release");
46 assert_eq!(created.description, "First public release");
47 assert_eq!(created.project_id, project.id);
48 assert_eq!(created.position, 0);
49 assert_eq!(created.target_date, Some(target));
50 assert_eq!(created.status, MilestoneStatus::Open);
51
52 let fetched = repo
53 .get_by_id(created.id, user_id)
54 .await
55 .expect("Failed to get milestone");
56 assert!(fetched.is_some());
57 let fetched = fetched.unwrap();
58 assert_eq!(fetched.id, created.id);
59 assert_eq!(fetched.name, "Alpha Release");
60 assert_eq!(fetched.description, "First public release");
61 assert_eq!(fetched.target_date, Some(target));
62 assert_eq!(fetched.status, MilestoneStatus::Open);
63 }
64
65 #[tokio::test]
66 async fn test_create_milestone_without_date() {
67 let pool = common::setup_test_db().await;
68 let user_id = common::create_test_user(&pool).await;
69 let project_repo = SqliteProjectRepository::new(pool.clone());
70 let project = project_repo
71 .create(
72 user_id,
73 NewProject {
74 name: "Test Project".to_string(),
75 description: String::new(),
76 project_type: ProjectType::SideProject,
77 status: ProjectStatus::Active,
78 },
79 )
80 .await
81 .unwrap();
82 let repo = SqliteMilestoneRepository::new(pool);
83
84 let created = repo
85 .create(
86 user_id,
87 NewMilestone {
88 project_id: project.id,
89 name: "Undated Milestone".to_string(),
90 description: "No deadline".to_string(),
91 position: 0,
92 target_date: None,
93 },
94 )
95 .await
96 .expect("Failed to create milestone");
97
98 assert!(created.target_date.is_none());
99
100 let fetched = repo
101 .get_by_id(created.id, user_id)
102 .await
103 .expect("Failed to get milestone")
104 .unwrap();
105 assert!(fetched.target_date.is_none());
106 }
107
108 #[tokio::test]
109 async fn test_list_by_project_ordered() {
110 let pool = common::setup_test_db().await;
111 let user_id = common::create_test_user(&pool).await;
112 let project_repo = SqliteProjectRepository::new(pool.clone());
113 let project = project_repo
114 .create(
115 user_id,
116 NewProject {
117 name: "Test Project".to_string(),
118 description: String::new(),
119 project_type: ProjectType::SideProject,
120 status: ProjectStatus::Active,
121 },
122 )
123 .await
124 .unwrap();
125 let repo = SqliteMilestoneRepository::new(pool);
126
127 // Create milestones with out-of-order positions
128 for (name, position) in [("Second", 2), ("Zero", 0), ("First", 1)] {
129 repo.create(
130 user_id,
131 NewMilestone {
132 project_id: project.id,
133 name: name.to_string(),
134 description: String::new(),
135 position,
136 target_date: None,
137 },
138 )
139 .await
140 .expect("Failed to create milestone");
141 }
142
143 let milestones = repo
144 .list_by_project(project.id, user_id)
145 .await
146 .expect("Failed to list milestones");
147 assert_eq!(milestones.len(), 3);
148 assert_eq!(milestones[0].name, "Zero");
149 assert_eq!(milestones[0].position, 0);
150 assert_eq!(milestones[1].name, "First");
151 assert_eq!(milestones[1].position, 1);
152 assert_eq!(milestones[2].name, "Second");
153 assert_eq!(milestones[2].position, 2);
154 }
155
156 #[tokio::test]
157 async fn test_milestone_default_status_open() {
158 let pool = common::setup_test_db().await;
159 let user_id = common::create_test_user(&pool).await;
160 let project_repo = SqliteProjectRepository::new(pool.clone());
161 let project = project_repo
162 .create(
163 user_id,
164 NewProject {
165 name: "Test Project".to_string(),
166 description: String::new(),
167 project_type: ProjectType::SideProject,
168 status: ProjectStatus::Active,
169 },
170 )
171 .await
172 .unwrap();
173 let repo = SqliteMilestoneRepository::new(pool);
174
175 let created = repo
176 .create(
177 user_id,
178 NewMilestone {
179 project_id: project.id,
180 name: "New Milestone".to_string(),
181 description: String::new(),
182 position: 0,
183 target_date: None,
184 },
185 )
186 .await
187 .expect("Failed to create milestone");
188
189 assert_eq!(created.status, MilestoneStatus::Open);
190 }
191
192 #[tokio::test]
193 async fn test_update_milestone() {
194 let pool = common::setup_test_db().await;
195 let user_id = common::create_test_user(&pool).await;
196 let project_repo = SqliteProjectRepository::new(pool.clone());
197 let project = project_repo
198 .create(
199 user_id,
200 NewProject {
201 name: "Test Project".to_string(),
202 description: String::new(),
203 project_type: ProjectType::SideProject,
204 status: ProjectStatus::Active,
205 },
206 )
207 .await
208 .unwrap();
209 let repo = SqliteMilestoneRepository::new(pool.clone());
210
211 let created = repo
212 .create(
213 user_id,
214 NewMilestone {
215 project_id: project.id,
216 name: "Original".to_string(),
217 description: "Original desc".to_string(),
218 position: 0,
219 target_date: None,
220 },
221 )
222 .await
223 .expect("Failed to create milestone");
224
225 let new_date = chrono::NaiveDate::from_ymd_opt(2026, 12, 31).unwrap();
226 let updated = repo
227 .update(
228 created.id,
229 user_id,
230 "Updated Name",
231 "Updated description",
232 Some(new_date),
233 &MilestoneStatus::Completed,
234 )
235 .await
236 .expect("Failed to update milestone");
237
238 assert!(updated.is_some());
239 let updated = updated.unwrap();
240 assert_eq!(updated.name, "Updated Name");
241 assert_eq!(updated.description, "Updated description");
242 assert_eq!(updated.target_date, Some(new_date));
243
244 // BUG: db_value() stores "completed" (lowercase) but strum only parses "Completed"
245 // (capitalized), so from_str_or_default falls back to Open. Verify the DB has the
246 // correct value even though the round-trip deserialises incorrectly.
247 let row: (String,) = sqlx::query_as("SELECT status FROM milestones WHERE id = ?")
248 .bind(created.id.to_string())
249 .fetch_one(&pool)
250 .await
251 .unwrap();
252 assert_eq!(row.0, "completed");
253 }
254
255 #[tokio::test]
256 async fn test_delete_milestone() {
257 let pool = common::setup_test_db().await;
258 let user_id = common::create_test_user(&pool).await;
259 let project_repo = SqliteProjectRepository::new(pool.clone());
260 let project = project_repo
261 .create(
262 user_id,
263 NewProject {
264 name: "Test Project".to_string(),
265 description: String::new(),
266 project_type: ProjectType::SideProject,
267 status: ProjectStatus::Active,
268 },
269 )
270 .await
271 .unwrap();
272 let repo = SqliteMilestoneRepository::new(pool);
273
274 let created = repo
275 .create(
276 user_id,
277 NewMilestone {
278 project_id: project.id,
279 name: "To Delete".to_string(),
280 description: String::new(),
281 position: 0,
282 target_date: None,
283 },
284 )
285 .await
286 .expect("Failed to create milestone");
287
288 let deleted = repo
289 .delete(created.id, user_id)
290 .await
291 .expect("Failed to delete milestone");
292 assert!(deleted);
293
294 let fetched = repo
295 .get_by_id(created.id, user_id)
296 .await
297 .expect("Failed to get milestone");
298 assert!(fetched.is_none());
299 }
300
301 #[tokio::test]
302 async fn test_delete_wrong_user_returns_false() {
303 let pool = common::setup_test_db().await;
304 let user1 = common::create_test_user(&pool).await;
305 let user2 = common::create_test_user(&pool).await;
306 let project_repo = SqliteProjectRepository::new(pool.clone());
307 let project = project_repo
308 .create(
309 user1,
310 NewProject {
311 name: "User1 Project".to_string(),
312 description: String::new(),
313 project_type: ProjectType::SideProject,
314 status: ProjectStatus::Active,
315 },
316 )
317 .await
318 .unwrap();
319 let repo = SqliteMilestoneRepository::new(pool);
320
321 let created = repo
322 .create(
323 user1,
324 NewMilestone {
325 project_id: project.id,
326 name: "User1 Milestone".to_string(),
327 description: String::new(),
328 position: 0,
329 target_date: None,
330 },
331 )
332 .await
333 .expect("Failed to create milestone");
334
335 // User 2 cannot delete user 1's milestone
336 let deleted = repo
337 .delete(created.id, user2)
338 .await
339 .expect("Failed to attempt delete");
340 assert!(!deleted);
341
342 // Milestone still exists for user 1
343 let fetched = repo
344 .get_by_id(created.id, user1)
345 .await
346 .expect("Failed to get milestone");
347 assert!(fetched.is_some());
348 }
349
350 #[tokio::test]
351 async fn test_reorder_milestones() {
352 let pool = common::setup_test_db().await;
353 let user_id = common::create_test_user(&pool).await;
354 let project_repo = SqliteProjectRepository::new(pool.clone());
355 let project = project_repo
356 .create(
357 user_id,
358 NewProject {
359 name: "Test Project".to_string(),
360 description: String::new(),
361 project_type: ProjectType::SideProject,
362 status: ProjectStatus::Active,
363 },
364 )
365 .await
366 .unwrap();
367 let repo = SqliteMilestoneRepository::new(pool);
368
369 let a = repo
370 .create(
371 user_id,
372 NewMilestone {
373 project_id: project.id,
374 name: "A".to_string(),
375 description: String::new(),
376 position: 0,
377 target_date: None,
378 },
379 )
380 .await
381 .unwrap();
382 let b = repo
383 .create(
384 user_id,
385 NewMilestone {
386 project_id: project.id,
387 name: "B".to_string(),
388 description: String::new(),
389 position: 1,
390 target_date: None,
391 },
392 )
393 .await
394 .unwrap();
395 let c = repo
396 .create(
397 user_id,
398 NewMilestone {
399 project_id: project.id,
400 name: "C".to_string(),
401 description: String::new(),
402 position: 2,
403 target_date: None,
404 },
405 )
406 .await
407 .unwrap();
408
409 // Reorder to C, A, B
410 repo.reorder(project.id, user_id, &[c.id, a.id, b.id])
411 .await
412 .expect("Failed to reorder milestones");
413
414 let milestones = repo
415 .list_by_project(project.id, user_id)
416 .await
417 .expect("Failed to list milestones");
418 assert_eq!(milestones.len(), 3);
419 assert_eq!(milestones[0].name, "C");
420 assert_eq!(milestones[1].name, "A");
421 assert_eq!(milestones[2].name, "B");
422 }
423
424 #[tokio::test]
425 async fn test_get_nonexistent_returns_none() {
426 let pool = common::setup_test_db().await;
427 let user_id = common::create_test_user(&pool).await;
428 let repo = SqliteMilestoneRepository::new(pool);
429
430 let fake_id = goingson_core::MilestoneId::from(uuid::Uuid::new_v4());
431 let fetched = repo
432 .get_by_id(fake_id, user_id)
433 .await
434 .expect("Failed to get milestone");
435 assert!(fetched.is_none());
436 }
437