Skip to main content

max / goingson

11.0 KB · 336 lines History Blame Raw
1 //! Integration tests for SqliteSearchRepository.
2
3 mod common;
4
5 use goingson_core::{
6 NewProject, NewTask, Priority, ProjectRepository, ProjectStatus, ProjectType, SearchQuery,
7 SearchRepository, SearchResultType, TaskRepository, UserId,
8 };
9 use goingson_db_sqlite::{SqliteProjectRepository, SqliteSearchRepository, SqliteTaskRepository};
10
11 /// Helper: create a task with a specific description for FTS matching.
12 async fn create_task_with_desc(pool: &sqlx::SqlitePool, user_id: UserId, description: &str) {
13 let repo = SqliteTaskRepository::new(pool.clone());
14 let new_task = NewTask {
15 project_id: None,
16 description: description.to_string(),
17 priority: Priority::Medium,
18 due: None,
19 tags: vec![],
20 recurrence: goingson_core::Recurrence::None,
21 recurrence_rule: None,
22 urgency: 0.0,
23 source_email_id: None,
24 scheduled_start: None,
25 scheduled_duration: None,
26 contact_id: None,
27 milestone_id: None,
28 estimated_minutes: None,
29 recurrence_parent_id: None,
30 };
31 repo.create(user_id, new_task).await.unwrap();
32 }
33
34 /// Helper: create a task with a specific priority.
35 async fn create_task_with_priority(
36 pool: &sqlx::SqlitePool,
37 user_id: UserId,
38 description: &str,
39 priority: Priority,
40 ) {
41 let repo = SqliteTaskRepository::new(pool.clone());
42 let new_task = NewTask {
43 project_id: None,
44 description: description.to_string(),
45 priority,
46 due: None,
47 tags: vec![],
48 recurrence: goingson_core::Recurrence::None,
49 recurrence_rule: None,
50 urgency: 0.0,
51 source_email_id: None,
52 scheduled_start: None,
53 scheduled_duration: None,
54 contact_id: None,
55 milestone_id: None,
56 estimated_minutes: None,
57 recurrence_parent_id: None,
58 };
59 repo.create(user_id, new_task).await.unwrap();
60 }
61
62 /// Helper: create a project with a name.
63 async fn create_project(
64 pool: &sqlx::SqlitePool,
65 user_id: UserId,
66 name: &str,
67 ) -> goingson_core::Project {
68 let repo = SqliteProjectRepository::new(pool.clone());
69 repo.create(
70 user_id,
71 NewProject {
72 name: name.to_string(),
73 description: format!("Project for {}", name),
74 project_type: ProjectType::SideProject,
75 status: ProjectStatus::Active,
76 },
77 )
78 .await
79 .unwrap()
80 }
81
82 // ============ Core Search Tests ============
83
84 #[tokio::test]
85 async fn search_empty_query_returns_empty() {
86 let pool = common::setup_test_db().await;
87 let user_id = common::create_test_user(&pool).await;
88 let repo = SqliteSearchRepository::new(pool);
89
90 let query = SearchQuery {
91 query: String::new(),
92 ..Default::default()
93 };
94 let (results, _total) = repo.search(user_id, query).await.unwrap();
95 assert!(results.is_empty());
96 }
97
98 #[tokio::test]
99 async fn search_tasks_by_text() {
100 let pool = common::setup_test_db().await;
101 let user_id = common::create_test_user(&pool).await;
102
103 create_task_with_desc(&pool, user_id, "Fix login bug in auth module").await;
104 create_task_with_desc(&pool, user_id, "Update documentation for API").await;
105
106 let repo = SqliteSearchRepository::new(pool);
107 let query = SearchQuery::new("login");
108 let (results, _total) = repo.search(user_id, query).await.unwrap();
109
110 assert_eq!(results.len(), 1);
111 assert_eq!(results[0].result_type, SearchResultType::Task);
112 assert!(results[0].title.contains("login"));
113 }
114
115 #[tokio::test]
116 async fn search_projects_by_name() {
117 let pool = common::setup_test_db().await;
118 let user_id = common::create_test_user(&pool).await;
119
120 create_project(&pool, user_id, "GoingsOn Development").await;
121 create_project(&pool, user_id, "Website Redesign").await;
122
123 let repo = SqliteSearchRepository::new(pool);
124 let query = SearchQuery::new("redesign");
125 let (results, _total) = repo.search(user_id, query).await.unwrap();
126
127 assert!(results.iter().any(|r| r.result_type == SearchResultType::Project));
128 assert!(results.iter().any(|r| r.title.contains("Redesign")));
129 }
130
131 #[tokio::test]
132 async fn search_returns_multiple_types() {
133 let pool = common::setup_test_db().await;
134 let user_id = common::create_test_user(&pool).await;
135
136 create_task_with_desc(&pool, user_id, "Build authentication system").await;
137 create_project(&pool, user_id, "Authentication Service").await;
138
139 let repo = SqliteSearchRepository::new(pool);
140 let query = SearchQuery::new("authentication");
141 let (results, _total) = repo.search(user_id, query).await.unwrap();
142
143 let types: Vec<_> = results.iter().map(|r| &r.result_type).collect();
144 assert!(types.contains(&&SearchResultType::Task));
145 assert!(types.contains(&&SearchResultType::Project));
146 }
147
148 // ============ Filter Tests ============
149
150 #[tokio::test]
151 async fn search_type_filter() {
152 let pool = common::setup_test_db().await;
153 let user_id = common::create_test_user(&pool).await;
154
155 create_task_with_desc(&pool, user_id, "Deploy server infrastructure").await;
156 create_project(&pool, user_id, "Server Infrastructure").await;
157
158 let repo = SqliteSearchRepository::new(pool);
159 let query = SearchQuery::new("infrastructure")
160 .with_types(vec![SearchResultType::Task]);
161 let (results, _total) = repo.search(user_id, query).await.unwrap();
162
163 assert!(results.iter().all(|r| r.result_type == SearchResultType::Task));
164 }
165
166 #[tokio::test]
167 async fn search_priority_filter() {
168 let pool = common::setup_test_db().await;
169 let user_id = common::create_test_user(&pool).await;
170
171 create_task_with_priority(&pool, user_id, "High priority deploy task", Priority::High).await;
172 create_task_with_priority(&pool, user_id, "Low priority deploy cleanup", Priority::Low).await;
173
174 let repo = SqliteSearchRepository::new(pool);
175 let mut query = SearchQuery::new("deploy");
176 query.priority = Some(Priority::High);
177 let (results, _total) = repo.search(user_id, query).await.unwrap();
178
179 assert_eq!(results.len(), 1);
180 assert!(results[0].title.contains("High priority"));
181 }
182
183 #[tokio::test]
184 async fn search_tag_include() {
185 let pool = common::setup_test_db().await;
186 let user_id = common::create_test_user(&pool).await;
187
188 // Create tasks with tags via the repository
189 let task_repo = SqliteTaskRepository::new(pool.clone());
190 let task = task_repo
191 .create(
192 user_id,
193 NewTask {
194 project_id: None,
195 description: "Tagged task for search testing".to_string(),
196 priority: Priority::Medium,
197 due: None,
198 tags: vec!["backend".to_string()],
199 recurrence: goingson_core::Recurrence::None,
200 recurrence_rule: None,
201 urgency: 0.0,
202 source_email_id: None,
203 scheduled_start: None,
204 scheduled_duration: None,
205 contact_id: None,
206 milestone_id: None,
207 estimated_minutes: None,
208 recurrence_parent_id: None,
209 },
210 )
211 .await
212 .unwrap();
213
214 // Also create an untagged task with similar text
215 create_task_with_desc(&pool, user_id, "Untagged task for search testing").await;
216
217 let repo = SqliteSearchRepository::new(pool);
218 let mut query = SearchQuery::new("search testing");
219 query.tags_include = vec!["backend".to_string()];
220 let (results, _total) = repo.search(user_id, query).await.unwrap();
221
222 assert_eq!(results.len(), 1);
223 assert_eq!(results[0].id, *task.id);
224 }
225
226 #[tokio::test]
227 async fn search_tag_exclude() {
228 let pool = common::setup_test_db().await;
229 let user_id = common::create_test_user(&pool).await;
230
231 let task_repo = SqliteTaskRepository::new(pool.clone());
232 task_repo
233 .create(
234 user_id,
235 NewTask {
236 project_id: None,
237 description: "Excluded task for tag filtering".to_string(),
238 priority: Priority::Medium,
239 due: None,
240 tags: vec!["wontfix".to_string()],
241 recurrence: goingson_core::Recurrence::None,
242 recurrence_rule: None,
243 urgency: 0.0,
244 source_email_id: None,
245 scheduled_start: None,
246 scheduled_duration: None,
247 contact_id: None,
248 milestone_id: None,
249 estimated_minutes: None,
250 recurrence_parent_id: None,
251 },
252 )
253 .await
254 .unwrap();
255
256 create_task_with_desc(&pool, user_id, "Included task for tag filtering").await;
257
258 let repo = SqliteSearchRepository::new(pool);
259 let mut query = SearchQuery::new("tag filtering");
260 query.tags_exclude = vec!["wontfix".to_string()];
261 let (results, _total) = repo.search(user_id, query).await.unwrap();
262
263 assert_eq!(results.len(), 1);
264 assert!(results[0].title.contains("Included"));
265 }
266
267 // ============ Pagination Tests ============
268
269 #[tokio::test]
270 async fn search_with_limit() {
271 let pool = common::setup_test_db().await;
272 let user_id = common::create_test_user(&pool).await;
273
274 for i in 1..=5 {
275 create_task_with_desc(&pool, user_id, &format!("Pagination task number {}", i)).await;
276 }
277
278 let repo = SqliteSearchRepository::new(pool);
279 let query = SearchQuery::new("pagination").with_limit(2);
280 let (results, _total) = repo.search(user_id, query).await.unwrap();
281
282 assert_eq!(results.len(), 2);
283 }
284
285 #[tokio::test]
286 async fn search_with_offset() {
287 let pool = common::setup_test_db().await;
288 let user_id = common::create_test_user(&pool).await;
289
290 for i in 1..=5 {
291 create_task_with_desc(&pool, user_id, &format!("Offset task item {}", i)).await;
292 }
293
294 let repo = SqliteSearchRepository::new(pool);
295
296 // Get all results first
297 let all_query = SearchQuery::new("offset task");
298 let (all, _total) = repo.search(user_id, all_query).await.unwrap();
299
300 // Get with offset
301 let offset_query = SearchQuery::new("offset task").with_offset(2).with_limit(50);
302 let (offset, _total) = repo.search(user_id, offset_query).await.unwrap();
303
304 assert_eq!(offset.len(), all.len() - 2);
305 }
306
307 // ============ Edge Cases ============
308
309 #[tokio::test]
310 async fn search_special_characters_escaped() {
311 let pool = common::setup_test_db().await;
312 let user_id = common::create_test_user(&pool).await;
313
314 create_task_with_desc(&pool, user_id, "Normal task for safety").await;
315
316 let repo = SqliteSearchRepository::new(pool);
317 // These characters should be escaped by prepare_fts5_query, not crash
318 let query = SearchQuery::new("test*\"(query):special");
319 let results = repo.search(user_id, query).await;
320 assert!(results.is_ok());
321 }
322
323 #[tokio::test]
324 async fn search_no_results() {
325 let pool = common::setup_test_db().await;
326 let user_id = common::create_test_user(&pool).await;
327
328 create_task_with_desc(&pool, user_id, "A regular task").await;
329
330 let repo = SqliteSearchRepository::new(pool);
331 let query = SearchQuery::new("xyznonexistentquery");
332 let (results, _total) = repo.search(user_id, query).await.unwrap();
333
334 assert!(results.is_empty());
335 }
336