Skip to main content

max / goingson

8.7 KB · 288 lines History Blame Raw
1 //! Integration tests for SqliteStatsRepository.
2
3 mod common;
4
5 use chrono::{Duration, Utc};
6 use goingson_core::{
7 EmailRepository, EventRepository, NewEmail, NewEvent, NewProject, NewTask, Priority,
8 ProjectRepository, ProjectStatus, ProjectType, Recurrence, StatsRepository, TaskCrud,
9 };
10 use goingson_db_sqlite::{
11 SqliteEmailRepository, SqliteEventRepository, SqliteProjectRepository, SqliteStatsRepository,
12 SqliteTaskRepository,
13 };
14
15 #[tokio::test]
16 async fn test_dashboard_stats_empty_db_is_all_zeros() {
17 let pool = common::setup_test_db().await;
18 let user_id = common::create_test_user(&pool).await;
19 let repo = SqliteStatsRepository::new(pool);
20
21 let now = Utc::now();
22 let stats = repo
23 .get_dashboard_stats(
24 user_id,
25 now,
26 now - Duration::hours(1),
27 now + Duration::hours(12),
28 now + Duration::days(7),
29 )
30 .await
31 .expect("get_dashboard_stats");
32
33 assert_eq!(stats.tasks_due_today, 0);
34 assert_eq!(stats.tasks_due_this_week, 0);
35 assert_eq!(stats.overdue_count, 0);
36 assert_eq!(stats.unread_emails, 0);
37 assert_eq!(stats.upcoming_events, 0);
38 assert_eq!(stats.active_projects, 0);
39 assert!(stats.high_urgency_tasks.is_empty());
40 }
41
42 #[tokio::test]
43 async fn test_dashboard_stats_populated_counts_match() {
44 let pool = common::setup_test_db().await;
45 let user_id = common::create_test_user(&pool).await;
46
47 let tasks = SqliteTaskRepository::new(pool.clone());
48 let events = SqliteEventRepository::new(pool.clone());
49 let emails = SqliteEmailRepository::new(pool.clone());
50 let projects = SqliteProjectRepository::new(pool.clone());
51 let stats_repo = SqliteStatsRepository::new(pool);
52
53 let now = Utc::now();
54 let today_start = now - Duration::hours(1);
55 let tomorrow_start = now + Duration::hours(12);
56 let week_end = now + Duration::days(7);
57
58 // Task due later today (inside [today_start, tomorrow_start)).
59 tasks
60 .create(
61 user_id,
62 NewTask::builder("Due today")
63 .priority(Priority::High)
64 .due(now + Duration::hours(2))
65 .build(),
66 )
67 .await
68 .expect("create today task");
69 // Task due in three days: this week but not today.
70 tasks
71 .create(
72 user_id,
73 NewTask::builder("Due this week")
74 .priority(Priority::Medium)
75 .due(now + Duration::days(3))
76 .build(),
77 )
78 .await
79 .expect("create week task");
80 // Overdue task.
81 tasks
82 .create(
83 user_id,
84 NewTask::builder("Overdue")
85 .priority(Priority::Low)
86 .due(now - Duration::days(2))
87 .build(),
88 )
89 .await
90 .expect("create overdue task");
91 // Completed task is excluded from every task count and from high-urgency.
92 let done = tasks
93 .create(
94 user_id,
95 NewTask::builder("Already done")
96 .priority(Priority::High)
97 .due(now + Duration::hours(3))
98 .build(),
99 )
100 .await
101 .expect("create done task");
102 tasks.complete(done.id, user_id).await.expect("complete");
103
104 // One unread email (counted) and one read email (not counted).
105 emails
106 .create(
107 user_id,
108 NewEmail {
109 project_id: None,
110 from_address: "a@example.com".into(),
111 to_address: "me@example.com".into(),
112 subject: "Unread".into(),
113 body: "body".into(),
114 is_read: false,
115 received_at: Some(now),
116 },
117 )
118 .await
119 .expect("create unread email");
120 let read = emails
121 .create(
122 user_id,
123 NewEmail {
124 project_id: None,
125 from_address: "b@example.com".into(),
126 to_address: "me@example.com".into(),
127 subject: "Read".into(),
128 body: "body".into(),
129 is_read: true,
130 received_at: Some(now),
131 },
132 )
133 .await
134 .expect("create read email");
135 assert!(read.is_read);
136
137 // One upcoming event (within 7 days) and one far-future event (excluded).
138 events
139 .create(
140 user_id,
141 NewEvent {
142 user_id: Some(user_id),
143 project_id: None,
144 contact_id: None,
145 title: "Upcoming".into(),
146 description: String::new(),
147 start_time: now + Duration::days(1),
148 end_time: None,
149 location: None,
150 linked_task_id: None,
151 recurrence: Recurrence::None,
152 recurrence_rule: None,
153 block_type: None,
154 reminder_offsets_seconds: Vec::new(),
155 },
156 )
157 .await
158 .expect("create upcoming event");
159 events
160 .create(
161 user_id,
162 NewEvent {
163 user_id: Some(user_id),
164 project_id: None,
165 contact_id: None,
166 title: "Far off".into(),
167 description: String::new(),
168 start_time: now + Duration::days(30),
169 end_time: None,
170 location: None,
171 linked_task_id: None,
172 recurrence: Recurrence::None,
173 recurrence_rule: None,
174 block_type: None,
175 reminder_offsets_seconds: Vec::new(),
176 },
177 )
178 .await
179 .expect("create far event");
180
181 // One active project (counted) and one on-hold project (excluded).
182 projects
183 .create(
184 user_id,
185 NewProject {
186 name: "Active work".into(),
187 description: String::new(),
188 project_type: ProjectType::default(),
189 status: ProjectStatus::Active,
190 },
191 )
192 .await
193 .expect("create active project");
194 projects
195 .create(
196 user_id,
197 NewProject {
198 name: "Paused work".into(),
199 description: String::new(),
200 project_type: ProjectType::default(),
201 status: ProjectStatus::OnHold,
202 },
203 )
204 .await
205 .expect("create on-hold project");
206
207 let stats = stats_repo
208 .get_dashboard_stats(user_id, now, today_start, tomorrow_start, week_end)
209 .await
210 .expect("get_dashboard_stats");
211
212 assert_eq!(
213 stats.tasks_due_today, 1,
214 "only the task due later today counts"
215 );
216 assert_eq!(
217 stats.tasks_due_this_week, 2,
218 "today's task plus the three-day task"
219 );
220 assert_eq!(stats.overdue_count, 1, "only the past-due task counts");
221 assert_eq!(stats.unread_emails, 1, "only the unread email counts");
222 assert_eq!(
223 stats.upcoming_events, 1,
224 "only the within-a-week event counts"
225 );
226 assert_eq!(stats.active_projects, 1, "only the Active project counts");
227
228 // Three non-completed tasks remain; the completed one is excluded.
229 assert_eq!(
230 stats.high_urgency_tasks.len(),
231 3,
232 "completed task excluded from high-urgency list"
233 );
234 assert!(
235 stats
236 .high_urgency_tasks
237 .iter()
238 .all(|t| t.status != "Completed")
239 );
240 // The list is ordered by urgency descending.
241 for pair in stats.high_urgency_tasks.windows(2) {
242 assert!(
243 pair[0].urgency >= pair[1].urgency,
244 "high-urgency tasks must be sorted descending"
245 );
246 }
247 }
248
249 #[tokio::test]
250 async fn test_dashboard_stats_scoped_to_user() {
251 let pool = common::setup_test_db().await;
252 let mine = common::create_test_user(&pool).await;
253 let other = common::create_test_user(&pool).await;
254
255 let tasks = SqliteTaskRepository::new(pool.clone());
256 let stats_repo = SqliteStatsRepository::new(pool);
257
258 let now = Utc::now();
259 // Another user's overdue task must not leak into my dashboard.
260 tasks
261 .create(
262 other,
263 NewTask::builder("Not mine")
264 .priority(Priority::High)
265 .due(now - Duration::days(1))
266 .build(),
267 )
268 .await
269 .expect("create other task");
270
271 let stats = stats_repo
272 .get_dashboard_stats(
273 mine,
274 now,
275 now - Duration::hours(1),
276 now + Duration::hours(12),
277 now + Duration::days(7),
278 )
279 .await
280 .expect("get_dashboard_stats");
281
282 assert_eq!(
283 stats.overdue_count, 0,
284 "stats must be scoped to the requesting user"
285 );
286 assert!(stats.high_urgency_tasks.is_empty());
287 }
288