1mod account;
2mod body;
3mod draft;
4mod event_log;
5mod label;
6mod message;
7mod pool;
8mod rules;
9mod search;
10mod semantic;
11mod snooze;
12mod sync_cursor;
13mod sync_log;
14mod sync_runtime_status;
15mod thread;
16
17pub use event_log::EventLogEntry;
18pub use pool::Store;
19pub use rules::{row_to_rule_json, row_to_rule_log_json, RuleLogInput, RuleRecordInput};
20pub use sync_log::{SyncLogEntry, SyncStatus};
21pub use sync_runtime_status::{SyncRuntimeStatus, SyncRuntimeStatusUpdate};
22
23#[cfg(test)]
24mod tests {
25 use super::*;
26 use mxr_core::*;
27
28 fn test_account() -> Account {
29 Account {
30 id: AccountId::new(),
31 name: "Test".to_string(),
32 email: "test@example.com".to_string(),
33 sync_backend: Some(BackendRef {
34 provider_kind: ProviderKind::Fake,
35 config_key: "fake".to_string(),
36 }),
37 send_backend: None,
38 enabled: true,
39 }
40 }
41
42 fn test_envelope(account_id: &AccountId) -> Envelope {
43 Envelope {
44 id: MessageId::new(),
45 account_id: account_id.clone(),
46 provider_id: "fake-1".to_string(),
47 thread_id: ThreadId::new(),
48 message_id_header: Some("<test@example.com>".to_string()),
49 in_reply_to: None,
50 references: vec![],
51 from: Address {
52 name: Some("Alice".to_string()),
53 email: "alice@example.com".to_string(),
54 },
55 to: vec![Address {
56 name: None,
57 email: "bob@example.com".to_string(),
58 }],
59 cc: vec![],
60 bcc: vec![],
61 subject: "Test subject".to_string(),
62 date: chrono::Utc::now(),
63 flags: MessageFlags::READ | MessageFlags::STARRED,
64 snippet: "Preview text".to_string(),
65 has_attachments: false,
66 size_bytes: 1024,
67 unsubscribe: UnsubscribeMethod::None,
68 label_provider_ids: vec![],
69 }
70 }
71
72 #[tokio::test]
73 async fn account_roundtrip() {
74 let store = Store::in_memory().await.unwrap();
75 let account = test_account();
76 store.insert_account(&account).await.unwrap();
77 let fetched = store.get_account(&account.id).await.unwrap().unwrap();
78 assert_eq!(fetched.name, account.name);
79 assert_eq!(fetched.email, account.email);
80 }
81
82 #[tokio::test]
83 async fn account_insert_upserts_existing_runtime_record() {
84 let store = Store::in_memory().await.unwrap();
85 let mut account = test_account();
86 store.insert_account(&account).await.unwrap();
87
88 account.name = "Updated".to_string();
89 account.email = "updated@example.com".to_string();
90 store.insert_account(&account).await.unwrap();
91
92 let fetched = store.get_account(&account.id).await.unwrap().unwrap();
93 assert_eq!(fetched.name, "Updated");
94 assert_eq!(fetched.email, "updated@example.com");
95 }
96
97 #[tokio::test]
98 async fn envelope_upsert_and_query() {
99 let store = Store::in_memory().await.unwrap();
100 let account = test_account();
101 store.insert_account(&account).await.unwrap();
102
103 let env = test_envelope(&account.id);
104 store.upsert_envelope(&env).await.unwrap();
105
106 let fetched = store.get_envelope(&env.id).await.unwrap().unwrap();
107 assert_eq!(fetched.id, env.id);
108 assert_eq!(fetched.subject, env.subject);
109 assert_eq!(fetched.from.email, env.from.email);
110 assert_eq!(fetched.flags, env.flags);
111
112 let list = store
113 .list_envelopes_by_account(&account.id, 100, 0)
114 .await
115 .unwrap();
116 assert_eq!(list.len(), 1);
117 }
118
119 #[tokio::test]
120 async fn label_crud() {
121 let store = Store::in_memory().await.unwrap();
122 let account = test_account();
123 store.insert_account(&account).await.unwrap();
124
125 let label = Label {
126 id: LabelId::new(),
127 account_id: account.id.clone(),
128 name: "Inbox".to_string(),
129 kind: LabelKind::System,
130 color: None,
131 provider_id: "INBOX".to_string(),
132 unread_count: 5,
133 total_count: 20,
134 };
135 store.upsert_label(&label).await.unwrap();
136
137 let labels = store.list_labels_by_account(&account.id).await.unwrap();
138 assert_eq!(labels.len(), 1);
139 assert_eq!(labels[0].name, "Inbox");
140 assert_eq!(labels[0].unread_count, 5);
141
142 store.update_label_counts(&label.id, 3, 25).await.unwrap();
143 let labels = store.list_labels_by_account(&account.id).await.unwrap();
144 assert_eq!(labels[0].unread_count, 3);
145 assert_eq!(labels[0].total_count, 25);
146 }
147
148 #[tokio::test]
149 async fn body_cache() {
150 let store = Store::in_memory().await.unwrap();
151 let account = test_account();
152 store.insert_account(&account).await.unwrap();
153
154 let env = test_envelope(&account.id);
155 store.upsert_envelope(&env).await.unwrap();
156
157 let body = MessageBody {
158 message_id: env.id.clone(),
159 text_plain: Some("Hello world".to_string()),
160 text_html: Some("<p>Hello world</p>".to_string()),
161 attachments: vec![AttachmentMeta {
162 id: AttachmentId::new(),
163 message_id: env.id.clone(),
164 filename: "report.pdf".to_string(),
165 mime_type: "application/pdf".to_string(),
166 size_bytes: 50000,
167 local_path: None,
168 provider_id: "att-1".to_string(),
169 }],
170 fetched_at: chrono::Utc::now(),
171 metadata: MessageMetadata::default(),
172 };
173 store.insert_body(&body).await.unwrap();
174
175 let fetched = store.get_body(&env.id).await.unwrap().unwrap();
176 assert_eq!(fetched.text_plain, body.text_plain);
177 assert_eq!(fetched.attachments.len(), 1);
178 assert_eq!(fetched.attachments[0].filename, "report.pdf");
179 }
180
181 #[tokio::test]
182 async fn message_labels_junction() {
183 let store = Store::in_memory().await.unwrap();
184 let account = test_account();
185 store.insert_account(&account).await.unwrap();
186
187 let label = Label {
188 id: LabelId::new(),
189 account_id: account.id.clone(),
190 name: "Inbox".to_string(),
191 kind: LabelKind::System,
192 color: None,
193 provider_id: "INBOX".to_string(),
194 unread_count: 0,
195 total_count: 0,
196 };
197 store.upsert_label(&label).await.unwrap();
198
199 let env = test_envelope(&account.id);
200 store.upsert_envelope(&env).await.unwrap();
201 store
202 .set_message_labels(&env.id, std::slice::from_ref(&label.id))
203 .await
204 .unwrap();
205
206 let by_label = store
207 .list_envelopes_by_label(&label.id, 100, 0)
208 .await
209 .unwrap();
210 assert_eq!(by_label.len(), 1);
211 assert_eq!(by_label[0].id, env.id);
212 assert_eq!(by_label[0].label_provider_ids, vec!["INBOX".to_string()]);
213
214 let by_account = store
215 .list_envelopes_by_account(&account.id, 100, 0)
216 .await
217 .unwrap();
218 assert_eq!(by_account.len(), 1);
219 assert_eq!(by_account[0].label_provider_ids, vec!["INBOX".to_string()]);
220
221 let fetched = store.get_envelope(&env.id).await.unwrap().unwrap();
222 assert_eq!(fetched.label_provider_ids, vec!["INBOX".to_string()]);
223 }
224
225 #[tokio::test]
226 async fn thread_aggregation() {
227 let store = Store::in_memory().await.unwrap();
228 let account = test_account();
229 store.insert_account(&account).await.unwrap();
230
231 let thread_id = ThreadId::new();
232 for i in 0..3 {
233 let mut env = test_envelope(&account.id);
234 env.provider_id = format!("fake-thread-{}", i);
235 env.thread_id = thread_id.clone();
236 env.date = chrono::Utc::now() - chrono::Duration::hours(i);
237 if i == 0 {
238 env.flags = MessageFlags::empty(); }
240 store.upsert_envelope(&env).await.unwrap();
241 }
242
243 let thread = store.get_thread(&thread_id).await.unwrap().unwrap();
244 assert_eq!(thread.message_count, 3);
245 assert_eq!(thread.unread_count, 1);
246 }
247
248 #[tokio::test]
249 async fn list_subscriptions_groups_by_sender_and_skips_trash() {
250 let store = Store::in_memory().await.unwrap();
251 let account = test_account();
252 store.insert_account(&account).await.unwrap();
253
254 let mut latest = test_envelope(&account.id);
255 latest.from.name = Some("Readwise".into());
256 latest.from.email = "hello@readwise.io".into();
257 latest.subject = "Latest digest".into();
258 latest.unsubscribe = UnsubscribeMethod::HttpLink {
259 url: "https://example.com/unsub".into(),
260 };
261 latest.date = chrono::Utc::now();
262
263 let mut older = latest.clone();
264 older.id = MessageId::new();
265 older.provider_id = "fake-older".into();
266 older.subject = "Older digest".into();
267 older.date = latest.date - chrono::Duration::days(3);
268
269 let mut trashed = latest.clone();
270 trashed.id = MessageId::new();
271 trashed.provider_id = "fake-trash".into();
272 trashed.subject = "Trashed digest".into();
273 trashed.date = latest.date + chrono::Duration::hours(1);
274 trashed.flags.insert(MessageFlags::TRASH);
275
276 let mut no_unsub = test_envelope(&account.id);
277 no_unsub.from.email = "plain@example.com".into();
278 no_unsub.provider_id = "fake-none".into();
279 no_unsub.unsubscribe = UnsubscribeMethod::None;
280
281 store.upsert_envelope(&older).await.unwrap();
282 store.upsert_envelope(&latest).await.unwrap();
283 store.upsert_envelope(&trashed).await.unwrap();
284 store.upsert_envelope(&no_unsub).await.unwrap();
285
286 let subscriptions = store.list_subscriptions(10).await.unwrap();
287 assert_eq!(subscriptions.len(), 1);
288 assert_eq!(subscriptions[0].sender_email, "hello@readwise.io");
289 assert_eq!(subscriptions[0].message_count, 2);
290 assert_eq!(subscriptions[0].latest_subject, "Latest digest");
291 }
292
293 #[tokio::test]
294 async fn draft_crud() {
295 let store = Store::in_memory().await.unwrap();
296 let account = test_account();
297 store.insert_account(&account).await.unwrap();
298
299 let draft = Draft {
300 id: DraftId::new(),
301 account_id: account.id.clone(),
302 reply_headers: None,
303 to: vec![Address {
304 name: None,
305 email: "bob@example.com".to_string(),
306 }],
307 cc: vec![],
308 bcc: vec![],
309 subject: "Draft subject".to_string(),
310 body_markdown: "# Hello".to_string(),
311 attachments: vec![],
312 created_at: chrono::Utc::now(),
313 updated_at: chrono::Utc::now(),
314 };
315 store.insert_draft(&draft).await.unwrap();
316
317 let drafts = store.list_drafts(&account.id).await.unwrap();
318 assert_eq!(drafts.len(), 1);
319
320 store.delete_draft(&draft.id).await.unwrap();
321 let drafts = store.list_drafts(&account.id).await.unwrap();
322 assert_eq!(drafts.len(), 0);
323 }
324
325 #[tokio::test]
326 async fn snooze_lifecycle() {
327 let store = Store::in_memory().await.unwrap();
328 let account = test_account();
329 store.insert_account(&account).await.unwrap();
330
331 let env = test_envelope(&account.id);
332 store.upsert_envelope(&env).await.unwrap();
333
334 let snoozed = Snoozed {
335 message_id: env.id.clone(),
336 account_id: account.id.clone(),
337 snoozed_at: chrono::Utc::now(),
338 wake_at: chrono::Utc::now() - chrono::Duration::hours(1), original_labels: vec![],
340 };
341 store.insert_snooze(&snoozed).await.unwrap();
342
343 let due = store.get_due_snoozes(chrono::Utc::now()).await.unwrap();
344 assert_eq!(due.len(), 1);
345
346 store.remove_snooze(&env.id).await.unwrap();
347 let due = store.get_due_snoozes(chrono::Utc::now()).await.unwrap();
348 assert_eq!(due.len(), 0);
349 }
350
351 #[tokio::test]
352 async fn sync_log_lifecycle() {
353 let store = Store::in_memory().await.unwrap();
354 let account = test_account();
355 store.insert_account(&account).await.unwrap();
356
357 let log_id = store
358 .insert_sync_log(&account.id, &SyncStatus::Running)
359 .await
360 .unwrap();
361 assert!(log_id > 0);
362
363 store
364 .complete_sync_log(log_id, &SyncStatus::Success, 55, None)
365 .await
366 .unwrap();
367
368 let last = store.get_last_sync(&account.id).await.unwrap().unwrap();
369 assert_eq!(last.status, SyncStatus::Success);
370 assert_eq!(last.messages_synced, 55);
371 }
372
373 #[tokio::test]
374 async fn event_log_insert_and_query() {
375 let store = Store::in_memory().await.unwrap();
376 let account = test_account();
377 store.insert_account(&account).await.unwrap();
378 let env = test_envelope(&account.id);
379 let env_id = env.id.as_str();
380 store.upsert_envelope(&env).await.unwrap();
381
382 store
383 .insert_event("info", "sync", "Sync completed", Some(&account.id), None)
384 .await
385 .unwrap();
386 store
387 .insert_event(
388 "error",
389 "sync",
390 "Sync failed",
391 Some(&account.id),
392 Some("timeout"),
393 )
394 .await
395 .unwrap();
396 store
397 .insert_event("info", "rule", "Rule applied", None, None)
398 .await
399 .unwrap();
400 store
401 .insert_event_refs(
402 "info",
403 "mutation",
404 "Archived a message",
405 Some(&account.id),
406 Some(env_id.as_str()),
407 None,
408 Some("from=test@example.com"),
409 )
410 .await
411 .unwrap();
412
413 let all = store.list_events(10, None, None).await.unwrap();
414 assert_eq!(all.len(), 4);
415
416 let errors = store.list_events(10, Some("error"), None).await.unwrap();
417 assert_eq!(errors.len(), 1);
418 assert_eq!(errors[0].summary, "Sync failed");
419
420 let sync_events = store.list_events(10, None, Some("sync")).await.unwrap();
421 assert_eq!(sync_events.len(), 2);
422
423 let mutation_events = store.list_events(10, None, Some("mutation")).await.unwrap();
424 assert_eq!(mutation_events.len(), 1);
425 assert_eq!(
426 mutation_events[0].message_id.as_deref(),
427 Some(env_id.as_str())
428 );
429 }
430
431 #[tokio::test]
432 async fn prune_events_before_removes_old_rows() {
433 let store = Store::in_memory().await.unwrap();
434 let account = test_account();
435 store.insert_account(&account).await.unwrap();
436
437 store
438 .insert_event("info", "sync", "recent", Some(&account.id), None)
439 .await
440 .unwrap();
441 sqlx::query("UPDATE event_log SET timestamp = ? WHERE summary = 'recent'")
442 .bind(chrono::Utc::now().timestamp())
443 .execute(store.writer())
444 .await
445 .unwrap();
446
447 store
448 .insert_event("info", "sync", "old", Some(&account.id), None)
449 .await
450 .unwrap();
451 sqlx::query("UPDATE event_log SET timestamp = ? WHERE summary = 'old'")
452 .bind((chrono::Utc::now() - chrono::Duration::days(120)).timestamp())
453 .execute(store.writer())
454 .await
455 .unwrap();
456
457 let removed = store
458 .prune_events_before((chrono::Utc::now() - chrono::Duration::days(90)).timestamp())
459 .await
460 .unwrap();
461 assert_eq!(removed, 1);
462
463 let events = store.list_events(10, None, None).await.unwrap();
464 assert_eq!(events.len(), 1);
465 assert_eq!(events[0].summary, "recent");
466 }
467
468 #[tokio::test]
469 async fn get_message_id_by_provider_id() {
470 let store = Store::in_memory().await.unwrap();
471 let account = test_account();
472 store.insert_account(&account).await.unwrap();
473
474 let env = test_envelope(&account.id);
475 store.upsert_envelope(&env).await.unwrap();
476
477 let found = store
478 .get_message_id_by_provider_id(&account.id, &env.provider_id)
479 .await
480 .unwrap();
481 assert_eq!(found, Some(env.id.clone()));
482
483 let not_found = store
484 .get_message_id_by_provider_id(&account.id, "nonexistent")
485 .await
486 .unwrap();
487 assert!(not_found.is_none());
488 }
489
490 #[tokio::test]
491 async fn recalculate_label_counts() {
492 let store = Store::in_memory().await.unwrap();
493 let account = test_account();
494 store.insert_account(&account).await.unwrap();
495
496 let label = Label {
497 id: LabelId::new(),
498 account_id: account.id.clone(),
499 name: "Inbox".to_string(),
500 kind: LabelKind::System,
501 color: None,
502 provider_id: "INBOX".to_string(),
503 unread_count: 0,
504 total_count: 0,
505 };
506 store.upsert_label(&label).await.unwrap();
507
508 for i in 0..3 {
510 let mut env = test_envelope(&account.id);
511 env.provider_id = format!("fake-label-{}", i);
512 if i < 2 {
513 env.flags = MessageFlags::READ;
514 } else {
515 env.flags = MessageFlags::empty();
516 }
517 store.upsert_envelope(&env).await.unwrap();
518 store
519 .set_message_labels(&env.id, std::slice::from_ref(&label.id))
520 .await
521 .unwrap();
522 }
523
524 store.recalculate_label_counts(&account.id).await.unwrap();
525
526 let labels = store.list_labels_by_account(&account.id).await.unwrap();
527 assert_eq!(labels[0].total_count, 3);
528 assert_eq!(labels[0].unread_count, 1);
529 }
530
531 #[tokio::test]
532 async fn replace_label_moves_message_associations_when_id_changes() {
533 let store = Store::in_memory().await.unwrap();
534 let account = test_account();
535 store.insert_account(&account).await.unwrap();
536
537 let original = Label {
538 id: LabelId::from_provider_id("imap", "Projects"),
539 account_id: account.id.clone(),
540 name: "Projects".to_string(),
541 kind: LabelKind::Folder,
542 color: None,
543 provider_id: "Projects".to_string(),
544 unread_count: 0,
545 total_count: 0,
546 };
547 store.upsert_label(&original).await.unwrap();
548
549 let env = test_envelope(&account.id);
550 store.upsert_envelope(&env).await.unwrap();
551 store
552 .set_message_labels(&env.id, std::slice::from_ref(&original.id))
553 .await
554 .unwrap();
555
556 let renamed = Label {
557 id: LabelId::from_provider_id("imap", "Client Work"),
558 account_id: account.id.clone(),
559 name: "Client Work".to_string(),
560 kind: LabelKind::Folder,
561 color: None,
562 provider_id: "Client Work".to_string(),
563 unread_count: 0,
564 total_count: 0,
565 };
566 store.replace_label(&original.id, &renamed).await.unwrap();
567
568 let labels = store.list_labels_by_account(&account.id).await.unwrap();
569 assert_eq!(labels.len(), 1);
570 assert_eq!(labels[0].name, "Client Work");
571 assert_eq!(labels[0].id, renamed.id);
572
573 let by_new_label = store
574 .list_envelopes_by_label(&renamed.id, 100, 0)
575 .await
576 .unwrap();
577 assert_eq!(by_new_label.len(), 1);
578 assert_eq!(by_new_label[0].id, env.id);
579 assert!(store
580 .list_envelopes_by_label(&original.id, 100, 0)
581 .await
582 .unwrap()
583 .is_empty());
584 }
585
586 #[tokio::test]
587 async fn rules_roundtrip_and_history() {
588 let store = Store::in_memory().await.unwrap();
589 let now = chrono::Utc::now();
590
591 store
592 .upsert_rule(crate::RuleRecordInput {
593 id: "rule-1",
594 name: "Archive newsletters",
595 enabled: true,
596 priority: 10,
597 conditions_json: r#"{"type":"field","field":"has_label","label":"newsletters"}"#,
598 actions_json: r#"[{"type":"archive"}]"#,
599 created_at: now,
600 updated_at: now,
601 })
602 .await
603 .unwrap();
604
605 let rules = store.list_rules().await.unwrap();
606 assert_eq!(rules.len(), 1);
607 let rule_json = crate::rules::row_to_rule_json(&rules[0]);
608 assert_eq!(rule_json["name"], "Archive newsletters");
609 assert_eq!(rule_json["priority"], 10);
610
611 store
612 .insert_rule_log(crate::RuleLogInput {
613 rule_id: "rule-1",
614 rule_name: "Archive newsletters",
615 message_id: "msg-1",
616 actions_applied_json: r#"["archive"]"#,
617 timestamp: now,
618 success: true,
619 error: None,
620 })
621 .await
622 .unwrap();
623
624 let logs = store.list_rule_logs(Some("rule-1"), 10).await.unwrap();
625 assert_eq!(logs.len(), 1);
626 let log_json = crate::rules::row_to_rule_log_json(&logs[0]);
627 assert_eq!(log_json["rule_name"], "Archive newsletters");
628 assert_eq!(log_json["message_id"], "msg-1");
629 }
630
631 #[tokio::test]
632 async fn get_saved_search_by_name() {
633 let store = Store::in_memory().await.unwrap();
634
635 let search = SavedSearch {
636 id: SavedSearchId::new(),
637 account_id: None,
638 name: "Unread".to_string(),
639 query: "is:unread".to_string(),
640 search_mode: SearchMode::Lexical,
641 sort: SortOrder::DateDesc,
642 icon: None,
643 position: 0,
644 created_at: chrono::Utc::now(),
645 };
646 store.insert_saved_search(&search).await.unwrap();
647
648 let found = store.get_saved_search_by_name("Unread").await.unwrap();
649 assert!(found.is_some());
650 assert_eq!(found.unwrap().query, "is:unread");
651
652 let not_found = store.get_saved_search_by_name("Nonexistent").await.unwrap();
653 assert!(not_found.is_none());
654 }
655
656 #[tokio::test]
657 async fn saved_search_crud() {
658 let store = Store::in_memory().await.unwrap();
659
660 let s1 = SavedSearch {
661 id: SavedSearchId::new(),
662 account_id: None,
663 name: "Unread".to_string(),
664 query: "is:unread".to_string(),
665 search_mode: SearchMode::Lexical,
666 sort: SortOrder::DateDesc,
667 icon: None,
668 position: 0,
669 created_at: chrono::Utc::now(),
670 };
671 let s2 = SavedSearch {
672 id: SavedSearchId::new(),
673 account_id: None,
674 name: "Starred".to_string(),
675 query: "is:starred".to_string(),
676 search_mode: SearchMode::Hybrid,
677 sort: SortOrder::DateDesc,
678 icon: Some("star".to_string()),
679 position: 1,
680 created_at: chrono::Utc::now(),
681 };
682
683 store.insert_saved_search(&s1).await.unwrap();
684 store.insert_saved_search(&s2).await.unwrap();
685
686 let all = store.list_saved_searches().await.unwrap();
688 assert_eq!(all.len(), 2);
689 assert_eq!(all[0].name, "Unread");
690 assert_eq!(all[1].name, "Starred");
691
692 let found = store.get_saved_search_by_name("Starred").await.unwrap();
694 assert!(found.is_some());
695 assert_eq!(found.unwrap().query, "is:starred");
696
697 store.delete_saved_search(&s1.id).await.unwrap();
699 let remaining = store.list_saved_searches().await.unwrap();
700 assert_eq!(remaining.len(), 1);
701 assert_eq!(remaining[0].name, "Starred");
702
703 let deleted = store.delete_saved_search_by_name("Starred").await.unwrap();
705 assert!(deleted);
706 let empty = store.list_saved_searches().await.unwrap();
707 assert!(empty.is_empty());
708 }
709
710 #[tokio::test]
711 async fn list_contacts_ordered_by_frequency() {
712 let store = Store::in_memory().await.unwrap();
713 let account = test_account();
714 store.insert_account(&account).await.unwrap();
715
716 for i in 0..3 {
718 let mut env = test_envelope(&account.id);
719 env.provider_id = format!("fake-alice-{}", i);
720 env.from = Address {
721 name: Some("Alice".to_string()),
722 email: "alice@example.com".to_string(),
723 };
724 store.upsert_envelope(&env).await.unwrap();
725 }
726 for i in 0..2 {
727 let mut env = test_envelope(&account.id);
728 env.provider_id = format!("fake-bob-{}", i);
729 env.from = Address {
730 name: Some("Bob".to_string()),
731 email: "bob@example.com".to_string(),
732 };
733 store.upsert_envelope(&env).await.unwrap();
734 }
735 {
736 let mut env = test_envelope(&account.id);
737 env.provider_id = "fake-carol-0".to_string();
738 env.from = Address {
739 name: Some("Carol".to_string()),
740 email: "carol@example.com".to_string(),
741 };
742 store.upsert_envelope(&env).await.unwrap();
743 }
744
745 let contacts = store.list_contacts(10).await.unwrap();
746 assert_eq!(contacts.len(), 3);
747 assert_eq!(contacts[0].1, "alice@example.com");
749 assert_eq!(contacts[1].1, "bob@example.com");
750 assert_eq!(contacts[2].1, "carol@example.com");
751 }
752
753 #[tokio::test]
754 async fn sync_cursor_persistence() {
755 let store = Store::in_memory().await.unwrap();
756 let account = test_account();
757 store.insert_account(&account).await.unwrap();
758
759 let cursor = store.get_sync_cursor(&account.id).await.unwrap();
760 assert!(cursor.is_none());
761
762 let new_cursor = SyncCursor::Gmail { history_id: 12345 };
763 store
764 .set_sync_cursor(&account.id, &new_cursor)
765 .await
766 .unwrap();
767
768 let fetched = store.get_sync_cursor(&account.id).await.unwrap().unwrap();
769 let json = serde_json::to_string(&fetched).unwrap();
770 assert!(json.contains("12345"));
771 }
772}