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