| 4 |
4 |
|
|
| 5 |
5 |
|
use chrono::{Duration, Utc};
|
| 6 |
6 |
|
use goingson_core::{
|
| 7 |
|
- |
EmailRepository, EventRepository, NewEmail, NewEvent, NewProject, NewTask, Priority,
|
| 8 |
|
- |
ProjectRepository, ProjectStatus, ProjectType, Recurrence, StatsRepository, TaskCrud,
|
|
7 |
+ |
ContactRepository, EmailRepository, EventRepository, NewContact, NewEmail, NewEvent,
|
|
8 |
+ |
NewProject, NewTask, Priority, ProjectRepository, ProjectStatus, ProjectType, Recurrence,
|
|
9 |
+ |
StatsRepository, TaskCrud,
|
| 9 |
10 |
|
};
|
| 10 |
11 |
|
use goingson_db_sqlite::{
|
| 11 |
|
- |
SqliteEmailRepository, SqliteEventRepository, SqliteProjectRepository, SqliteStatsRepository,
|
| 12 |
|
- |
SqliteTaskRepository,
|
|
12 |
+ |
SqliteContactRepository, SqliteEmailRepository, SqliteEventRepository, SqliteProjectRepository,
|
|
13 |
+ |
SqliteStatsRepository, SqliteTaskRepository,
|
| 13 |
14 |
|
};
|
| 14 |
15 |
|
|
| 15 |
16 |
|
#[tokio::test]
|
| 285 |
286 |
|
);
|
| 286 |
287 |
|
assert!(stats.high_urgency_tasks.is_empty());
|
| 287 |
288 |
|
}
|
|
289 |
+ |
|
|
290 |
+ |
/// `get_export_counts` must agree with the `list_all` methods it stands in for.
|
|
291 |
+ |
///
|
|
292 |
+ |
/// The export summary used to be `list_all(..).len()` on five repositories,
|
|
293 |
+ |
/// which loaded every row (email bodies included) to produce five integers. It
|
|
294 |
+ |
/// is a COUNT query now, so the filters live in two places: each `list_all`
|
|
295 |
+ |
/// WHERE clause and the counts query. This test populates the rows those
|
|
296 |
+ |
/// filters disagree about -- a deleted task, a draft email, an archived email,
|
|
297 |
+ |
/// an implicit contact -- so a filter that drifts on one side fails here rather
|
|
298 |
+ |
/// than quietly showing the user the wrong number before a backup.
|
|
299 |
+ |
#[tokio::test]
|
|
300 |
+ |
async fn export_counts_match_list_all() {
|
|
301 |
+ |
let pool = common::setup_test_db().await;
|
|
302 |
+ |
let user_id = common::create_test_user(&pool).await;
|
|
303 |
+ |
|
|
304 |
+ |
let projects = SqliteProjectRepository::new(pool.clone());
|
|
305 |
+ |
let tasks = SqliteTaskRepository::new(pool.clone());
|
|
306 |
+ |
let events = SqliteEventRepository::new(pool.clone());
|
|
307 |
+ |
let emails = SqliteEmailRepository::new(pool.clone());
|
|
308 |
+ |
let contacts = SqliteContactRepository::new(pool.clone());
|
|
309 |
+ |
let stats = SqliteStatsRepository::new(pool.clone());
|
|
310 |
+ |
|
|
311 |
+ |
let now = Utc::now();
|
|
312 |
+ |
|
|
313 |
+ |
projects
|
|
314 |
+ |
.create(
|
|
315 |
+ |
user_id,
|
|
316 |
+ |
NewProject {
|
|
317 |
+ |
name: "Counted".to_string(),
|
|
318 |
+ |
description: String::new(),
|
|
319 |
+ |
project_type: ProjectType::default(),
|
|
320 |
+ |
status: ProjectStatus::Active,
|
|
321 |
+ |
},
|
|
322 |
+ |
)
|
|
323 |
+ |
.await
|
|
324 |
+ |
.expect("create project");
|
|
325 |
+ |
|
|
326 |
+ |
tasks
|
|
327 |
+ |
.create(user_id, NewTask::builder("Kept").build())
|
|
328 |
+ |
.await
|
|
329 |
+ |
.expect("create kept task");
|
|
330 |
+ |
// Soft-deleted: excluded by list_all's `status != 'Deleted'`.
|
|
331 |
+ |
let doomed = tasks
|
|
332 |
+ |
.create(user_id, NewTask::builder("Deleted").build())
|
|
333 |
+ |
.await
|
|
334 |
+ |
.expect("create doomed task");
|
|
335 |
+ |
tasks.delete(doomed.id, user_id).await.expect("delete task");
|
|
336 |
+ |
|
|
337 |
+ |
events
|
|
338 |
+ |
.create(
|
|
339 |
+ |
user_id,
|
|
340 |
+ |
NewEvent {
|
|
341 |
+ |
user_id: Some(user_id),
|
|
342 |
+ |
project_id: None,
|
|
343 |
+ |
contact_id: None,
|
|
344 |
+ |
title: "Counted".into(),
|
|
345 |
+ |
description: String::new(),
|
|
346 |
+ |
start_time: now,
|
|
347 |
+ |
end_time: Some(now + Duration::hours(1)),
|
|
348 |
+ |
location: None,
|
|
349 |
+ |
linked_task_id: None,
|
|
350 |
+ |
recurrence: Recurrence::None,
|
|
351 |
+ |
recurrence_rule: None,
|
|
352 |
+ |
block_type: None,
|
|
353 |
+ |
reminder_offsets_seconds: Vec::new(),
|
|
354 |
+ |
},
|
|
355 |
+ |
)
|
|
356 |
+ |
.await
|
|
357 |
+ |
.expect("create event");
|
|
358 |
+ |
|
|
359 |
+ |
let plain = emails
|
|
360 |
+ |
.create(
|
|
361 |
+ |
user_id,
|
|
362 |
+ |
NewEmail {
|
|
363 |
+ |
project_id: None,
|
|
364 |
+ |
from_address: "a@example.com".to_string(),
|
|
365 |
+ |
to_address: "b@example.com".to_string(),
|
|
366 |
+ |
subject: "Plain".to_string(),
|
|
367 |
+ |
body: "body".to_string(),
|
|
368 |
+ |
is_read: false,
|
|
369 |
+ |
received_at: Some(now),
|
|
370 |
+ |
},
|
|
371 |
+ |
)
|
|
372 |
+ |
.await
|
|
373 |
+ |
.expect("create email");
|
|
374 |
+ |
// Archived: still counted, because the export passes include_archived = true.
|
|
375 |
+ |
let archived = emails
|
|
376 |
+ |
.create(
|
|
377 |
+ |
user_id,
|
|
378 |
+ |
NewEmail {
|
|
379 |
+ |
project_id: None,
|
|
380 |
+ |
from_address: "a@example.com".to_string(),
|
|
381 |
+ |
to_address: "b@example.com".to_string(),
|
|
382 |
+ |
subject: "Archived".to_string(),
|
|
383 |
+ |
body: "body".to_string(),
|
|
384 |
+ |
is_read: true,
|
|
385 |
+ |
received_at: Some(now),
|
|
386 |
+ |
},
|
|
387 |
+ |
)
|
|
388 |
+ |
.await
|
|
389 |
+ |
.expect("create archived email");
|
|
390 |
+ |
emails
|
|
391 |
+ |
.archive(archived.id, user_id)
|
|
392 |
+ |
.await
|
|
393 |
+ |
.expect("archive email");
|
|
394 |
+ |
// Draft: excluded by list_all's `is_draft = 0`.
|
|
395 |
+ |
emails
|
|
396 |
+ |
.save_draft(
|
|
397 |
+ |
goingson_core::EmailId::new(),
|
|
398 |
+ |
user_id,
|
|
399 |
+ |
"a@example.com",
|
|
400 |
+ |
"b@example.com",
|
|
401 |
+ |
None,
|
|
402 |
+ |
None,
|
|
403 |
+ |
"Draft",
|
|
404 |
+ |
"body",
|
|
405 |
+ |
None,
|
|
406 |
+ |
None,
|
|
407 |
+ |
None,
|
|
408 |
+ |
None,
|
|
409 |
+ |
)
|
|
410 |
+ |
.await
|
|
411 |
+ |
.expect("save draft");
|
|
412 |
+ |
assert_ne!(plain.id, archived.id);
|
|
413 |
+ |
|
|
414 |
+ |
for (name, is_implicit) in [("Explicit", false), ("Implicit", true)] {
|
|
415 |
+ |
contacts
|
|
416 |
+ |
.create(
|
|
417 |
+ |
user_id,
|
|
418 |
+ |
NewContact {
|
|
419 |
+ |
display_name: name.to_string(),
|
|
420 |
+ |
nickname: None,
|
|
421 |
+ |
company: None,
|
|
422 |
+ |
title: None,
|
|
423 |
+ |
notes: String::new(),
|
|
424 |
+ |
tags: vec![],
|
|
425 |
+ |
birthday: None,
|
|
426 |
+ |
timezone: None,
|
|
427 |
+ |
is_implicit,
|
|
428 |
+ |
},
|
|
429 |
+ |
)
|
|
430 |
+ |
.await
|
|
431 |
+ |
.expect("create contact");
|
|
432 |
+ |
}
|
|
433 |
+ |
|
|
434 |
+ |
let counts = stats
|
|
435 |
+ |
.get_export_counts(user_id)
|
|
436 |
+ |
.await
|
|
437 |
+ |
.expect("get_export_counts");
|
|
438 |
+ |
|
|
439 |
+ |
assert_eq!(
|
|
440 |
+ |
counts.projects as usize,
|
|
441 |
+ |
projects.list_all(user_id).await.unwrap().len(),
|
|
442 |
+ |
"project count"
|
|
443 |
+ |
);
|
|
444 |
+ |
assert_eq!(
|
|
445 |
+ |
counts.tasks as usize,
|
|
446 |
+ |
tasks.list_all(user_id).await.unwrap().len(),
|
|
447 |
+ |
"task count excludes the deleted task"
|
|
448 |
+ |
);
|
|
449 |
+ |
assert_eq!(
|
|
450 |
+ |
counts.events as usize,
|
|
451 |
+ |
events.list_all(user_id).await.unwrap().len(),
|
|
452 |
+ |
"event count"
|
|
453 |
+ |
);
|
|
454 |
+ |
assert_eq!(
|
|
455 |
+ |
counts.emails as usize,
|
|
456 |
+ |
emails.list_all(user_id, true).await.unwrap().len(),
|
|
457 |
+ |
"email count keeps the archived email and drops the draft"
|
|
458 |
+ |
);
|
|
459 |
+ |
assert_eq!(
|
|
460 |
+ |
counts.contacts as usize,
|
|
461 |
+ |
contacts.list_all(user_id).await.unwrap().len(),
|
|
462 |
+ |
"contact count excludes the implicit contact"
|
|
463 |
+ |
);
|
|
464 |
+ |
|
|
465 |
+ |
// Pin the absolute numbers too, so a filter that drifts on *both* sides at
|
|
466 |
+ |
// once (making the equalities above vacuously true) still fails.
|
|
467 |
+ |
assert_eq!(counts.tasks, 1, "one kept task");
|
|
468 |
+ |
assert_eq!(counts.emails, 2, "plain + archived, not the draft");
|
|
469 |
+ |
assert_eq!(counts.contacts, 1, "one explicit contact");
|
|
470 |
+ |
}
|