Skip to main content

max / goingson

7.5 KB · 204 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(user_id, now, now - Duration::hours(1), now + Duration::hours(12), now + Duration::days(7))
24 .await
25 .expect("get_dashboard_stats");
26
27 assert_eq!(stats.tasks_due_today, 0);
28 assert_eq!(stats.tasks_due_this_week, 0);
29 assert_eq!(stats.overdue_count, 0);
30 assert_eq!(stats.unread_emails, 0);
31 assert_eq!(stats.upcoming_events, 0);
32 assert_eq!(stats.active_projects, 0);
33 assert!(stats.high_urgency_tasks.is_empty());
34 }
35
36 #[tokio::test]
37 async fn test_dashboard_stats_populated_counts_match() {
38 let pool = common::setup_test_db().await;
39 let user_id = common::create_test_user(&pool).await;
40
41 let tasks = SqliteTaskRepository::new(pool.clone());
42 let events = SqliteEventRepository::new(pool.clone());
43 let emails = SqliteEmailRepository::new(pool.clone());
44 let projects = SqliteProjectRepository::new(pool.clone());
45 let stats_repo = SqliteStatsRepository::new(pool);
46
47 let now = Utc::now();
48 let today_start = now - Duration::hours(1);
49 let tomorrow_start = now + Duration::hours(12);
50 let week_end = now + Duration::days(7);
51
52 // Task due later today (inside [today_start, tomorrow_start)).
53 tasks
54 .create(user_id, NewTask::builder("Due today").priority(Priority::High).due(now + Duration::hours(2)).build())
55 .await
56 .expect("create today task");
57 // Task due in three days: this week but not today.
58 tasks
59 .create(user_id, NewTask::builder("Due this week").priority(Priority::Medium).due(now + Duration::days(3)).build())
60 .await
61 .expect("create week task");
62 // Overdue task.
63 tasks
64 .create(user_id, NewTask::builder("Overdue").priority(Priority::Low).due(now - Duration::days(2)).build())
65 .await
66 .expect("create overdue task");
67 // Completed task is excluded from every task count and from high-urgency.
68 let done = tasks
69 .create(user_id, NewTask::builder("Already done").priority(Priority::High).due(now + Duration::hours(3)).build())
70 .await
71 .expect("create done task");
72 tasks.complete(done.id, user_id).await.expect("complete");
73
74 // One unread email (counted) and one read email (not counted).
75 emails
76 .create(user_id, NewEmail {
77 project_id: None,
78 from_address: "a@example.com".into(),
79 to_address: "me@example.com".into(),
80 subject: "Unread".into(),
81 body: "body".into(),
82 is_read: false,
83 received_at: Some(now),
84 })
85 .await
86 .expect("create unread email");
87 let read = emails
88 .create(user_id, NewEmail {
89 project_id: None,
90 from_address: "b@example.com".into(),
91 to_address: "me@example.com".into(),
92 subject: "Read".into(),
93 body: "body".into(),
94 is_read: true,
95 received_at: Some(now),
96 })
97 .await
98 .expect("create read email");
99 assert!(read.is_read);
100
101 // One upcoming event (within 7 days) and one far-future event (excluded).
102 events
103 .create(user_id, NewEvent {
104 user_id: Some(user_id),
105 project_id: None,
106 contact_id: None,
107 title: "Upcoming".into(),
108 description: String::new(),
109 start_time: now + Duration::days(1),
110 end_time: None,
111 location: None,
112 linked_task_id: None,
113 recurrence: Recurrence::None,
114 recurrence_rule: None,
115 block_type: None,
116 reminder_offsets_seconds: Vec::new(),
117 })
118 .await
119 .expect("create upcoming event");
120 events
121 .create(user_id, NewEvent {
122 user_id: Some(user_id),
123 project_id: None,
124 contact_id: None,
125 title: "Far off".into(),
126 description: String::new(),
127 start_time: now + Duration::days(30),
128 end_time: None,
129 location: None,
130 linked_task_id: None,
131 recurrence: Recurrence::None,
132 recurrence_rule: None,
133 block_type: None,
134 reminder_offsets_seconds: Vec::new(),
135 })
136 .await
137 .expect("create far event");
138
139 // One active project (counted) and one on-hold project (excluded).
140 projects
141 .create(user_id, NewProject {
142 name: "Active work".into(),
143 description: String::new(),
144 project_type: ProjectType::default(),
145 status: ProjectStatus::Active,
146 })
147 .await
148 .expect("create active project");
149 projects
150 .create(user_id, NewProject {
151 name: "Paused work".into(),
152 description: String::new(),
153 project_type: ProjectType::default(),
154 status: ProjectStatus::OnHold,
155 })
156 .await
157 .expect("create on-hold project");
158
159 let stats = stats_repo
160 .get_dashboard_stats(user_id, now, today_start, tomorrow_start, week_end)
161 .await
162 .expect("get_dashboard_stats");
163
164 assert_eq!(stats.tasks_due_today, 1, "only the task due later today counts");
165 assert_eq!(stats.tasks_due_this_week, 2, "today's task plus the three-day task");
166 assert_eq!(stats.overdue_count, 1, "only the past-due task counts");
167 assert_eq!(stats.unread_emails, 1, "only the unread email counts");
168 assert_eq!(stats.upcoming_events, 1, "only the within-a-week event counts");
169 assert_eq!(stats.active_projects, 1, "only the Active project counts");
170
171 // Three non-completed tasks remain; the completed one is excluded.
172 assert_eq!(stats.high_urgency_tasks.len(), 3, "completed task excluded from high-urgency list");
173 assert!(stats.high_urgency_tasks.iter().all(|t| t.status != "Completed"));
174 // The list is ordered by urgency descending.
175 for pair in stats.high_urgency_tasks.windows(2) {
176 assert!(pair[0].urgency >= pair[1].urgency, "high-urgency tasks must be sorted descending");
177 }
178 }
179
180 #[tokio::test]
181 async fn test_dashboard_stats_scoped_to_user() {
182 let pool = common::setup_test_db().await;
183 let mine = common::create_test_user(&pool).await;
184 let other = common::create_test_user(&pool).await;
185
186 let tasks = SqliteTaskRepository::new(pool.clone());
187 let stats_repo = SqliteStatsRepository::new(pool);
188
189 let now = Utc::now();
190 // Another user's overdue task must not leak into my dashboard.
191 tasks
192 .create(other, NewTask::builder("Not mine").priority(Priority::High).due(now - Duration::days(1)).build())
193 .await
194 .expect("create other task");
195
196 let stats = stats_repo
197 .get_dashboard_stats(mine, now, now - Duration::hours(1), now + Duration::hours(12), now + Duration::days(7))
198 .await
199 .expect("get_dashboard_stats");
200
201 assert_eq!(stats.overdue_count, 0, "stats must be scoped to the requesting user");
202 assert!(stats.high_urgency_tasks.is_empty());
203 }
204