Skip to main content

max / goingson

12.2 KB · 265 lines History Blame Raw
1 //! Integration tests for SqliteTaskRepository focus + scheduling + reporting methods.
2
3 mod common;
4
5 use chrono::{Duration, TimeZone, Utc};
6 use goingson_core::{NewTask, Priority, TaskCrud, TaskScheduling};
7 use goingson_db_sqlite::SqliteTaskRepository;
8
9 // ---- Focus ----
10
11 #[tokio::test]
12 async fn test_set_focus_and_list_focused() {
13 let pool = common::setup_test_db().await;
14 let user_id = common::create_test_user(&pool).await;
15 let repo = SqliteTaskRepository::new(pool);
16
17 let task = repo.create(user_id, NewTask::builder("Focus me").build()).await.unwrap();
18
19 // Not focused initially.
20 assert!(repo.list_focused(user_id).await.unwrap().is_empty());
21
22 let focused = repo.set_focus(task.id, user_id, true).await.expect("set focus").unwrap();
23 assert!(focused.is_focus);
24 assert!(focused.focus_set_at.is_some(), "focus_set_at should be stamped");
25
26 let list = repo.list_focused(user_id).await.unwrap();
27 assert_eq!(list.len(), 1);
28 assert_eq!(list[0].id, task.id);
29
30 // Clearing focus removes it from the set.
31 let unfocused = repo.set_focus(task.id, user_id, false).await.expect("clear focus").unwrap();
32 assert!(!unfocused.is_focus);
33 assert!(unfocused.focus_set_at.is_none(), "focus_set_at should be nulled");
34 assert!(repo.list_focused(user_id).await.unwrap().is_empty());
35 }
36
37 #[tokio::test]
38 async fn test_clear_all_focus() {
39 let pool = common::setup_test_db().await;
40 let user_id = common::create_test_user(&pool).await;
41 let repo = SqliteTaskRepository::new(pool);
42
43 let a = repo.create(user_id, NewTask::builder("A").build()).await.unwrap();
44 let b = repo.create(user_id, NewTask::builder("B").build()).await.unwrap();
45 repo.set_focus(a.id, user_id, true).await.unwrap();
46 repo.set_focus(b.id, user_id, true).await.unwrap();
47 assert_eq!(repo.list_focused(user_id).await.unwrap().len(), 2);
48
49 let cleared = repo.clear_all_focus(user_id).await.expect("clear all");
50 assert_eq!(cleared, 2, "both focused tasks should be cleared");
51 assert!(repo.list_focused(user_id).await.unwrap().is_empty());
52
53 // A second call clears nothing.
54 assert_eq!(repo.clear_all_focus(user_id).await.unwrap(), 0);
55 }
56
57 #[tokio::test]
58 async fn test_focus_is_per_user_scoped() {
59 let pool = common::setup_test_db().await;
60 let user_a = common::create_test_user(&pool).await;
61 let user_b = common::create_test_user(&pool).await;
62 let repo = SqliteTaskRepository::new(pool);
63
64 let task_a = repo.create(user_a, NewTask::builder("A's task").build()).await.unwrap();
65 let task_b = repo.create(user_b, NewTask::builder("B's task").build()).await.unwrap();
66 repo.set_focus(task_a.id, user_a, true).await.unwrap();
67 repo.set_focus(task_b.id, user_b, true).await.unwrap();
68
69 // Each user only sees their own focused task.
70 let a_list = repo.list_focused(user_a).await.unwrap();
71 assert_eq!(a_list.len(), 1);
72 assert_eq!(a_list[0].id, task_a.id);
73
74 // Clearing all focus for A must not touch B.
75 repo.clear_all_focus(user_a).await.unwrap();
76 assert!(repo.list_focused(user_a).await.unwrap().is_empty());
77 let b_list = repo.list_focused(user_b).await.unwrap();
78 assert_eq!(b_list.len(), 1, "clearing A's focus must not affect B");
79 assert_eq!(b_list[0].id, task_b.id);
80 }
81
82 #[tokio::test]
83 async fn test_list_available_for_focus_candidate_set() {
84 let pool = common::setup_test_db().await;
85 let user_id = common::create_test_user(&pool).await;
86 let repo = SqliteTaskRepository::new(pool);
87
88 // Candidate: pending, not focused, not snoozed, not waiting.
89 let candidate = repo.create(user_id, NewTask::builder("Candidate").priority(Priority::High).build()).await.unwrap();
90
91 // Excluded: already focused.
92 let focused = repo.create(user_id, NewTask::builder("Focused").build()).await.unwrap();
93 repo.set_focus(focused.id, user_id, true).await.unwrap();
94
95 // Excluded: snoozed into the future.
96 let snoozed = repo.create(user_id, NewTask::builder("Snoozed").build()).await.unwrap();
97 repo.snooze(snoozed.id, user_id, Utc::now() + Duration::hours(2)).await.unwrap();
98
99 // Excluded: waiting for response.
100 let waiting = repo.create(user_id, NewTask::builder("Waiting").build()).await.unwrap();
101 repo.mark_waiting(waiting.id, user_id, None).await.unwrap();
102
103 // Excluded: completed.
104 let done = repo.create(user_id, NewTask::builder("Done").build()).await.unwrap();
105 repo.complete(done.id, user_id).await.unwrap();
106
107 let available = repo.list_available_for_focus(user_id, 100).await.expect("list available");
108 let ids: Vec<_> = available.iter().map(|t| t.id).collect();
109 assert_eq!(ids, vec![candidate.id], "only the pending unfocused task should be a focus candidate");
110 }
111
112 // ---- Scheduling ----
113
114 #[tokio::test]
115 async fn test_update_schedule_persists_and_clears() {
116 let pool = common::setup_test_db().await;
117 let user_id = common::create_test_user(&pool).await;
118 let repo = SqliteTaskRepository::new(pool);
119
120 let task = repo.create(user_id, NewTask::builder("Schedule me").build()).await.unwrap();
121 assert!(task.scheduled_start.is_none());
122 assert!(task.scheduled_duration.is_none());
123
124 let start = Utc.with_ymd_and_hms(2026, 8, 15, 14, 0, 0).unwrap();
125 let updated = repo.update_schedule(task.id, user_id, Some(start), Some(90)).await.expect("schedule").unwrap();
126 assert_eq!(updated.scheduled_start, Some(start));
127 assert_eq!(updated.scheduled_duration, Some(90));
128
129 // Re-fetch to confirm persistence.
130 let fetched = repo.get_by_id(task.id, user_id).await.unwrap().unwrap();
131 assert_eq!(fetched.scheduled_start, Some(start));
132 assert_eq!(fetched.scheduled_duration, Some(90));
133
134 // Passing None clears the schedule.
135 let cleared = repo.update_schedule(task.id, user_id, None, None).await.expect("clear schedule").unwrap();
136 assert!(cleared.scheduled_start.is_none());
137 assert!(cleared.scheduled_duration.is_none());
138 let refetched = repo.get_by_id(task.id, user_id).await.unwrap().unwrap();
139 assert!(refetched.scheduled_start.is_none());
140 assert!(refetched.scheduled_duration.is_none());
141 }
142
143 #[tokio::test]
144 async fn test_list_scheduled_for_date() {
145 let pool = common::setup_test_db().await;
146 let user_id = common::create_test_user(&pool).await;
147 let repo = SqliteTaskRepository::new(pool);
148
149 let on_date = repo.create(user_id, NewTask::builder("On date").build()).await.unwrap();
150 let off_date = repo.create(user_id, NewTask::builder("Off date").build()).await.unwrap();
151 let _unscheduled = repo.create(user_id, NewTask::builder("Unscheduled").build()).await.unwrap();
152
153 let target = Utc.with_ymd_and_hms(2026, 8, 15, 9, 30, 0).unwrap();
154 let other = Utc.with_ymd_and_hms(2026, 8, 16, 9, 30, 0).unwrap();
155 repo.update_schedule(on_date.id, user_id, Some(target), Some(60)).await.unwrap();
156 repo.update_schedule(off_date.id, user_id, Some(other), Some(60)).await.unwrap();
157
158 let date = chrono::NaiveDate::from_ymd_opt(2026, 8, 15).unwrap();
159 let scheduled = repo.list_scheduled_for_date(user_id, date).await.expect("list scheduled");
160 let ids: Vec<_> = scheduled.iter().map(|t| t.id).collect();
161 assert_eq!(ids, vec![on_date.id], "only the task scheduled on the target date should match");
162 }
163
164 #[tokio::test]
165 async fn test_list_scheduled_for_date_excludes_completed() {
166 let pool = common::setup_test_db().await;
167 let user_id = common::create_test_user(&pool).await;
168 let repo = SqliteTaskRepository::new(pool);
169
170 let task = repo.create(user_id, NewTask::builder("Scheduled then done").build()).await.unwrap();
171 let start = Utc.with_ymd_and_hms(2026, 8, 15, 9, 30, 0).unwrap();
172 repo.update_schedule(task.id, user_id, Some(start), Some(60)).await.unwrap();
173
174 let date = chrono::NaiveDate::from_ymd_opt(2026, 8, 15).unwrap();
175 assert_eq!(repo.list_scheduled_for_date(user_id, date).await.unwrap().len(), 1);
176
177 repo.complete(task.id, user_id).await.unwrap();
178 assert!(
179 repo.list_scheduled_for_date(user_id, date).await.unwrap().is_empty(),
180 "completed tasks should not appear in the scheduled-for-date list"
181 );
182 }
183
184 // ---- Reporting windows ----
185
186 #[tokio::test]
187 async fn test_list_due_between_inclusive_boundaries() {
188 let pool = common::setup_test_db().await;
189 let user_id = common::create_test_user(&pool).await;
190 let repo = SqliteTaskRepository::new(pool);
191
192 let start = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap();
193 let end = Utc.with_ymd_and_hms(2026, 9, 30, 23, 59, 59).unwrap();
194
195 let at_start = repo.create(user_id, NewTask::builder("At start").due(start).build()).await.unwrap();
196 let at_end = repo.create(user_id, NewTask::builder("At end").due(end).build()).await.unwrap();
197 let inside = repo.create(user_id, NewTask::builder("Inside").due(Utc.with_ymd_and_hms(2026, 9, 15, 12, 0, 0).unwrap()).build()).await.unwrap();
198 let _before = repo.create(user_id, NewTask::builder("Before").due(start - Duration::seconds(1)).build()).await.unwrap();
199 let _after = repo.create(user_id, NewTask::builder("After").due(end + Duration::seconds(1)).build()).await.unwrap();
200
201 let found = repo.list_due_between(user_id, start, end).await.expect("list due between");
202 let ids: Vec<_> = found.iter().map(|t| t.id).collect();
203 assert_eq!(ids.len(), 3, "window is inclusive on both bounds and excludes tasks just outside");
204 for want in [at_start.id, at_end.id, inside.id] {
205 assert!(ids.contains(&want), "expected task {want} in window");
206 }
207 }
208
209 #[tokio::test]
210 async fn test_list_due_between_excludes_completed() {
211 let pool = common::setup_test_db().await;
212 let user_id = common::create_test_user(&pool).await;
213 let repo = SqliteTaskRepository::new(pool);
214
215 let start = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap();
216 let end = Utc.with_ymd_and_hms(2026, 9, 30, 23, 59, 59).unwrap();
217
218 let task = repo.create(user_id, NewTask::builder("Due and done").due(Utc.with_ymd_and_hms(2026, 9, 10, 0, 0, 0).unwrap()).build()).await.unwrap();
219 assert_eq!(repo.list_due_between(user_id, start, end).await.unwrap().len(), 1);
220 repo.complete(task.id, user_id).await.unwrap();
221 assert!(repo.list_due_between(user_id, start, end).await.unwrap().is_empty());
222 }
223
224 #[tokio::test]
225 async fn test_list_became_overdue_between_window() {
226 let pool = common::setup_test_db().await;
227 let user_id = common::create_test_user(&pool).await;
228 let repo = SqliteTaskRepository::new(pool);
229
230 let start = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap();
231 let end = Utc.with_ymd_and_hms(2026, 9, 30, 23, 59, 59).unwrap();
232
233 let inside = repo.create(user_id, NewTask::builder("Overdue inside").due(Utc.with_ymd_and_hms(2026, 9, 15, 8, 0, 0).unwrap()).build()).await.unwrap();
234 let _outside = repo.create(user_id, NewTask::builder("Overdue outside").due(end + Duration::seconds(1)).build()).await.unwrap();
235 // Completed tasks are excluded even if due in range.
236 let done = repo.create(user_id, NewTask::builder("Overdue done").due(Utc.with_ymd_and_hms(2026, 9, 20, 8, 0, 0).unwrap()).build()).await.unwrap();
237 repo.complete(done.id, user_id).await.unwrap();
238
239 let found = repo.list_became_overdue_between(user_id, start, end).await.expect("list overdue between");
240 let ids: Vec<_> = found.iter().map(|t| t.id).collect();
241 assert_eq!(ids, vec![inside.id], "only the non-completed task due inside the window should match");
242 }
243
244 #[tokio::test]
245 async fn test_list_created_between_window() {
246 let pool = common::setup_test_db().await;
247 let user_id = common::create_test_user(&pool).await;
248 let repo = SqliteTaskRepository::new(pool);
249
250 let a = repo.create(user_id, NewTask::builder("Created one").build()).await.unwrap();
251 let b = repo.create(user_id, NewTask::builder("Created two").build()).await.unwrap();
252
253 // A window around now includes both freshly created tasks.
254 let start = Utc::now() - Duration::minutes(5);
255 let end = Utc::now() + Duration::minutes(5);
256 let found = repo.list_created_between(user_id, start, end).await.expect("list created between");
257 let ids: Vec<_> = found.iter().map(|t| t.id).collect();
258 assert!(ids.contains(&a.id) && ids.contains(&b.id), "both tasks created inside the window should match");
259
260 // A past window matches nothing.
261 let old_start = Utc::now() - Duration::days(30);
262 let old_end = Utc::now() - Duration::days(29);
263 assert!(repo.list_created_between(user_id, old_start, old_end).await.unwrap().is_empty());
264 }
265