Skip to main content

max / goingson

5.5 KB · 195 lines History Blame Raw
1 //! Integration tests for milestone repository operations.
2
3 use chrono::NaiveDate;
4 use goingson_core::{MilestoneStatus, NewMilestone, NewTask, Priority, TaskStatus};
5
6 use crate::test_utils::{create_test_project, setup_test_state};
7
8 #[tokio::test]
9 async fn create_and_list_milestone() {
10 let (state, user_id) = setup_test_state().await;
11 let project_id = create_test_project(&state, user_id).await;
12
13 let target = NaiveDate::from_ymd_opt(2026, 8, 15).unwrap();
14 let created = state
15 .milestones
16 .create(
17 user_id,
18 NewMilestone {
19 project_id,
20 name: "Alpha Release".to_string(),
21 description: "Ship it".to_string(),
22 position: 0,
23 target_date: Some(target),
24 },
25 )
26 .await
27 .unwrap();
28
29 assert_eq!(created.name, "Alpha Release");
30 assert_eq!(created.description, "Ship it");
31 assert_eq!(created.target_date, Some(target));
32 assert_eq!(created.status, MilestoneStatus::Open);
33
34 let milestones = state
35 .milestones
36 .list_by_project(project_id, user_id)
37 .await
38 .unwrap();
39 assert_eq!(milestones.len(), 1);
40 assert_eq!(milestones[0].id, created.id);
41 }
42
43 #[tokio::test]
44 async fn update_milestone() {
45 let (state, user_id) = setup_test_state().await;
46 let project_id = create_test_project(&state, user_id).await;
47
48 let created = state
49 .milestones
50 .create(
51 user_id,
52 NewMilestone {
53 project_id,
54 name: "Original".to_string(),
55 description: "old".to_string(),
56 position: 0,
57 target_date: None,
58 },
59 )
60 .await
61 .unwrap();
62
63 let new_date = NaiveDate::from_ymd_opt(2026, 12, 31).unwrap();
64 let updated = state
65 .milestones
66 .update(
67 created.id,
68 user_id,
69 "Renamed",
70 "new desc",
71 Some(new_date),
72 &MilestoneStatus::Completed,
73 )
74 .await
75 .unwrap()
76 .unwrap();
77
78 assert_eq!(updated.name, "Renamed");
79 assert_eq!(updated.description, "new desc");
80 assert_eq!(updated.target_date, Some(new_date));
81 }
82
83 #[tokio::test]
84 async fn update_nonexistent_milestone_returns_none() {
85 let (state, user_id) = setup_test_state().await;
86
87 let fake_id = goingson_core::MilestoneId::from(uuid::Uuid::new_v4());
88 let result = state
89 .milestones
90 .update(
91 fake_id,
92 user_id,
93 "Nope",
94 "",
95 None,
96 &MilestoneStatus::Open,
97 )
98 .await
99 .unwrap();
100 assert!(result.is_none());
101 }
102
103 #[tokio::test]
104 async fn delete_milestone() {
105 let (state, user_id) = setup_test_state().await;
106 let project_id = create_test_project(&state, user_id).await;
107
108 let created = state
109 .milestones
110 .create(
111 user_id,
112 NewMilestone {
113 project_id,
114 name: "To Delete".to_string(),
115 description: String::new(),
116 position: 0,
117 target_date: None,
118 },
119 )
120 .await
121 .unwrap();
122
123 assert!(state.milestones.delete(created.id, user_id).await.unwrap());
124 assert!(state
125 .milestones
126 .get_by_id(created.id, user_id)
127 .await
128 .unwrap()
129 .is_none());
130 // Deleting an already-gone milestone reports no rows affected.
131 assert!(!state.milestones.delete(created.id, user_id).await.unwrap());
132 }
133
134 /// Mirrors the progress fields the `list_milestones`/`update_milestone` commands
135 /// pre-compute: task_count, completed_count, and rounded percentage from the tasks
136 /// linked to a milestone.
137 #[tokio::test]
138 async fn milestone_progress_reflects_linked_tasks() {
139 let (state, user_id) = setup_test_state().await;
140 let project_id = create_test_project(&state, user_id).await;
141
142 let milestone = state
143 .milestones
144 .create(
145 user_id,
146 NewMilestone {
147 project_id,
148 name: "Progress".to_string(),
149 description: String::new(),
150 position: 0,
151 target_date: None,
152 },
153 )
154 .await
155 .unwrap();
156
157 // 4 tasks on the milestone, 3 complete; plus 1 unrelated task on the project.
158 for i in 0..4 {
159 let t = NewTask::builder(format!("Milestone task {i}"))
160 .project_id(project_id)
161 .milestone_id(milestone.id)
162 .priority(Priority::Medium)
163 .build();
164 let task = state.tasks.create(user_id, t).await.unwrap();
165 if i < 3 {
166 state.tasks.complete(task.id, user_id).await.unwrap();
167 }
168 }
169 let unrelated = NewTask::builder("Not on milestone")
170 .project_id(project_id)
171 .priority(Priority::Low)
172 .build();
173 state.tasks.create(user_id, unrelated).await.unwrap();
174
175 let all_tasks = state
176 .tasks
177 .list_by_project(user_id, project_id)
178 .await
179 .unwrap();
180 let milestone_tasks: Vec<_> = all_tasks
181 .iter()
182 .filter(|t| t.milestone_id == Some(milestone.id))
183 .collect();
184 let task_count = milestone_tasks.len();
185 let completed_count = milestone_tasks
186 .iter()
187 .filter(|t| t.status == TaskStatus::Completed)
188 .count();
189 let progress = ((completed_count as f64 / task_count as f64) * 100.0).round() as u8;
190
191 assert_eq!(task_count, 4);
192 assert_eq!(completed_count, 3);
193 assert_eq!(progress, 75);
194 }
195