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