1use super::DeviceSyncError;
2use crate::{context::XmtpSharedContext, groups::MlsGroup};
3use futures::{Stream, StreamExt};
4pub use xmtp_archive::*;
5use xmtp_db::{consent_record::StoredConsentRecord, group_message::StoredGroupMessage, prelude::*};
6use xmtp_proto::xmtp::device_sync::{BackupElement, backup_element::Element};
7
8use xmtp_events::{ArchiveRestored, ClientEvent, EventWriter};
9#[derive(Default)]
10struct ImportContext {
11 changed: bool,
12}
13
14pub async fn insert_importer(
15 importer: &mut ArchiveImporter,
16 context: &impl XmtpSharedContext,
17) -> Result<(), DeviceSyncError> {
18 insert_elements(importer, context).await
19}
20
21async fn insert_elements<S, E>(
22 elements: &mut S,
23 context: &impl XmtpSharedContext,
24) -> Result<(), DeviceSyncError>
25where
26 S: Stream<Item = Result<BackupElement, E>> + Unpin,
27 DeviceSyncError: From<E>,
28{
29 let mut import_ctx = ImportContext::default();
30 let result = async {
31 while let Some(element) = elements.next().await {
32 let element = element.map_err(DeviceSyncError::from)?;
33 insert(element, context, &mut import_ctx)?;
35 }
36 Ok(())
37 }
38 .await;
39 if import_ctx.changed {
40 context.events().emit(
41 Some(ClientEvent::ArchiveRestored(ArchiveRestored {
42 complete: result.is_ok(),
43 })),
44 None,
45 );
46 }
47 result
48}
49
50fn insert(
51 element: BackupElement,
52 context: &impl XmtpSharedContext,
53 import_context: &mut ImportContext,
54) -> Result<(), DeviceSyncError> {
55 let Some(element) = element.element else {
56 return Ok(());
57 };
58
59 match element {
60 Element::Consent(consent) => {
61 let consent: StoredConsentRecord = consent.try_into()?;
62 import_context.changed |= context.db().insert_newer_consent_record(consent)?.applied;
64 }
65 Element::Group(save) => {
66 import_context.changed |= MlsGroup::restore_from_archive(context, &save)?;
67 }
68 Element::GroupMessage(message) => {
69 let message: StoredGroupMessage = message.try_into()?;
70 import_context.changed |= message.store_or_ignore_changed(&context.db())?;
71 }
72 _ => {}
73 }
74
75 Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80 #![allow(unused)]
81 use super::*;
82 use crate::groups::send_message_opts::SendMessageOpts;
83 use crate::tester;
84 use crate::utils::{LocalTester, Tester};
85 use crate::worker::device_sync::{ArchiveOptions, BackupElementSelection};
86 use crate::{builder::ClientBuilder, utils::test::wait_for_min_intents};
87 use diesel::prelude::*;
88 use futures::AsyncReadExt;
89 use futures::io::{BufReader, Cursor};
90 use std::{path::Path, sync::Arc};
91 use xmtp_archive::exporter;
92 use xmtp_cryptography::utils::generate_local_wallet;
93 use xmtp_db::group_message::MsgQueryArgs;
94 use xmtp_db::{ConnectionExt, group::GroupMembershipState};
95 use xmtp_db::{
96 consent_record::StoredConsentRecord,
97 group::StoredGroup,
98 group_message::StoredGroupMessage,
99 schema::{consent_records, group_messages, groups},
100 };
101 use xmtp_mls_common::{
102 group::{DMMetadataOptions, GroupMetadataOptions},
103 group_mutable_metadata::MessageDisappearingSettings,
104 };
105 use xmtp_proto::types::GroupId;
106
107 struct WriteDuringExport<F: FnMut()> {
110 archive: Vec<u8>,
111 on_first: Option<F>,
112 }
113
114 impl<F: FnMut()> std::io::Write for WriteDuringExport<F> {
115 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
116 if self.archive.len() >= 2 + xmtp_archive::NONCE_SIZE {
117 self.on_first.take().into_iter().for_each(|mut f| f());
118 }
119 self.archive.write(buf)
120 }
121
122 fn flush(&mut self) -> std::io::Result<()> {
123 Ok(())
124 }
125 }
126
127 #[xmtp_common::test(unwrap_try = true)]
133 async fn archive_export_fails_on_undecodable_group_metadata() {
134 use openmls::{
135 component::ComponentData,
136 group::MlsGroup,
137 messages::proposals::{AppDataUpdateProposal, Proposal},
138 };
139 use openmls_traits::OpenMlsProvider;
140 use xmtp_archive::{ArchiveError, UnreadableGroup};
141 use xmtp_mls_common::app_data::component_id::ComponentId;
142
143 tester!(alix, disable_workers);
144 let conversation = alix.create_group(None, None)?;
145 let provider = alix.context.mls_provider();
146 let signer = &alix.context.identity().installation_keys;
147 let mut group = MlsGroup::load(
148 alix.context.mls_storage(),
149 &conversation.group_id.to_openmls(),
150 )??;
151 let malformed = vec![0xff; 3];
152 let mut stage = group
153 .commit_builder()
154 .add_proposal(Proposal::AppDataUpdate(Box::new(
155 AppDataUpdateProposal::update(ComponentId::ADMIN_LIST.as_u16(), malformed.clone()),
156 )))
157 .load_psks(provider.storage())?;
158 let mut updater = stage.app_data_dictionary_updater();
159 updater.set(ComponentData::from_parts(
160 ComponentId::ADMIN_LIST.as_u16(),
161 malformed.into(),
162 ));
163 stage.with_app_data_dictionary_updates(updater.changes());
164 stage
165 .build(provider.rand(), provider.crypto(), signer, |_| true)?
166 .stage_commit(&provider)?;
167 group.merge_pending_commit(&provider)?;
168
169 let opts = ArchiveOptions {
170 start_ns: None,
171 end_ns: None,
172 elements: vec![BackupElementSelection::Messages],
173 exclude_disappearing_messages: false,
174 };
175 let key = xmtp_common::rand_vec::<32>();
176 let failure = exporter::export(opts, alix.db(), &key, Vec::new());
177 assert!(
178 matches!(
179 failure,
180 Err(ArchiveError::UnreadableGroup {
181 group_id,
182 source: UnreadableGroup::MutableMetadata(_),
183 }) if group_id == conversation.group_id
184 ),
185 "export dropped an undecodable metadata value: {failure:?}"
186 );
187 }
188
189 #[xmtp_common::test(unwrap_try = true)]
199 async fn archive_snapshot_is_complete() {
200 use xmtp_db::consent_record::{ConsentState, ConsentType};
201 use xmtp_proto::xmtp::device_sync::{
202 backup_element::Element, consent_backup::ConsentStateSave,
203 };
204
205 tester!(alix, persistent_db, disable_workers);
206 tester!(bo, disable_workers);
207 let empty_group = alix.create_group(None, None)?;
208 let empty_dm = alix.find_or_create_dm(bo.inbox_id(), None).await?;
209 let outside = alix.create_group(None, None)?;
210 let outside_message = outside.send_message_optimistic(b"before", Default::default())?;
211 let old = alix.create_group(None, None)?;
212 let start_ns = xmtp_common::time::now_ns();
213 xmtp_common::time::sleep(std::time::Duration::from_millis(2)).await;
216 let in_window = old.send_message_optimistic(&[7; 512 * 1024], Default::default())?;
218 let consent = |entity: &str, state| {
219 StoredConsentRecord::new(ConsentType::InboxId, state, entity.to_string())
220 };
221 alix.db()
222 .insert_or_replace_consent_records(&[consent("carol", ConsentState::Allowed)])?;
223
224 let opts = ArchiveOptions {
225 elements: vec![
226 BackupElementSelection::Messages,
227 BackupElementSelection::Consent,
228 ],
229 start_ns: Some(start_ns),
230 end_ns: None,
231 exclude_disappearing_messages: false,
232 };
233 let key = vec![7; 32];
234 let writer = alix.db();
235 let mut sink = WriteDuringExport {
236 archive: vec![],
237 on_first: Some(|| {
238 let written = writer.insert_or_replace_consent_records(&[
239 consent("carol", ConsentState::Denied),
240 consent("dave", ConsentState::Allowed),
241 ]);
242 #[cfg(not(target_arch = "wasm32"))]
246 written.unwrap();
247 #[cfg(target_arch = "wasm32")]
248 assert!(written.is_err(), "wasm wrote during the export");
249 }),
250 };
251 exporter::export(opts, alix.db(), &key, &mut sink)?;
252 assert!(
253 sink.on_first.is_none(),
254 "the export never wrote mid-snapshot"
255 );
256 let archive = sink.archive;
257
258 let reader = Box::pin(BufReader::new(Cursor::new(archive.clone())));
259 let consent: Vec<_> = ArchiveImporter::load(reader, &key)
260 .await?
261 .filter_map(|e| async move {
262 match e.ok()?.element? {
263 Element::Consent(c) if c.entity == "carol" || c.entity == "dave" => Some(c),
264 _ => None,
265 }
266 })
267 .collect()
268 .await;
269 assert!(
270 matches!(
271 consent.as_slice(),
272 [c] if c.entity == "carol" && c.state == ConsentStateSave::Allowed as i32
273 ),
274 "the export saw writes committed after its snapshot: {consent:?}"
275 );
276
277 tester!(alix2, from: alix, disable_workers);
278 let events = alix2.context.events().subscribe(
279 xmtp_events::EventFilter::new([
280 xmtp_events::EventKind::ArchiveRestored,
281 xmtp_events::EventKind::ConversationJoined,
282 xmtp_events::EventKind::MessageReceived,
283 xmtp_events::EventKind::ConsentChanged,
284 ]),
285 Some(64),
286 );
287 let reader = Box::pin(BufReader::new(Cursor::new(archive)));
288 let mut importer = ArchiveImporter::load(reader, &key).await?;
289 insert_importer(&mut importer, &alix2.context).await?;
290
291 let db = alix2.db();
292 for group in [&empty_group, &empty_dm, &outside, &old] {
293 assert_eq!(
294 db.find_group(&group.group_id)??.membership_state,
295 GroupMembershipState::Restored
296 );
297 }
298 assert!(db.get_group_message(&in_window)?.is_some());
299 assert!(db.get_group_message(&outside_message)?.is_none());
300 assert!(
301 matches!(events.drain().as_slice(), [xmtp_events::EventEnvelope { client: Some(ClientEvent::ArchiveRestored(restored)), .. }] if restored.complete)
302 );
303 }
304
305 #[xmtp_common::test(unwrap_try = true)]
307 async fn partial_import_reports_incomplete_after_a_stored_change() {
308 tester!(alix, disable_workers);
309 let events = alix.context.events().subscribe(
310 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
311 Some(4),
312 );
313 let record = StoredConsentRecord::new(
314 xmtp_db::consent_record::ConsentType::InboxId,
315 xmtp_db::consent_record::ConsentState::Allowed,
316 "failed-import".into(),
317 );
318 let element = BackupElement {
319 element: Some(Element::Consent(record.into())),
320 };
321 let mut elements =
322 futures::stream::iter([Ok(element), Err(std::io::Error::other("archive cut"))]);
323 assert!(insert_elements(&mut elements, &alix.context).await.is_err());
324 assert!(
325 alix.db()
326 .get_consent_record(
327 "failed-import".into(),
328 xmtp_db::consent_record::ConsentType::InboxId,
329 )?
330 .is_some()
331 );
332 assert!(matches!(
333 events.drain().as_slice(),
334 [xmtp_events::EventEnvelope {
335 client: Some(ClientEvent::ArchiveRestored(restored)), ..
336 }] if !restored.complete
337 ));
338 }
339
340 #[xmtp_common::test(unwrap_try = true)]
342 async fn failed_import_suppresses_events_for_all_stored_elements() {
343 tester!(alix, disable_workers);
344 tester!(bo, disable_workers);
345 let group = alix.create_group(None, None)?;
346 let message_id = group.send_message_optimistic(b"archive message", Default::default())?;
347 alix.db()
348 .insert_or_replace_consent_records(&[StoredConsentRecord::new(
349 xmtp_db::consent_record::ConsentType::InboxId,
350 xmtp_db::consent_record::ConsentState::Denied,
351 bo.inbox_id().into(),
352 )])?;
353 let key = vec![9; 32];
354 let mut archive = vec![];
355 exporter::export(
356 ArchiveOptions {
357 elements: vec![
358 BackupElementSelection::Messages,
359 BackupElementSelection::Consent,
360 ],
361 start_ns: None,
362 end_ns: None,
363 exclude_disappearing_messages: false,
364 },
365 alix.db(),
366 &key,
367 &mut archive,
368 )?;
369 let importer =
370 ArchiveImporter::load(Box::pin(BufReader::new(Cursor::new(archive))), &key).await?;
371 let mut elements = importer
372 .map(|item| item.map_err(DeviceSyncError::from))
373 .chain(futures::stream::iter([Err(DeviceSyncError::from(
374 std::io::Error::other("archive cut"),
375 ))]));
376 tester!(restored, from: alix, disable_workers);
377 let events = restored.context.events().subscribe(
378 xmtp_events::EventFilter::new([
379 xmtp_events::EventKind::ArchiveRestored,
380 xmtp_events::EventKind::ConversationJoined,
381 xmtp_events::EventKind::MessageReceived,
382 xmtp_events::EventKind::ConsentChanged,
383 ]),
384 Some(64),
385 );
386 assert!(
387 insert_elements(&mut elements, &restored.context)
388 .await
389 .is_err()
390 );
391 assert!(restored.db().find_group(&group.group_id)?.is_some());
392 assert!(restored.db().get_group_message(&message_id)?.is_some());
393 assert!(
394 restored
395 .db()
396 .get_consent_record(
397 bo.inbox_id().into(),
398 xmtp_db::consent_record::ConsentType::InboxId
399 )?
400 .is_some()
401 );
402 assert!(
403 matches!(events.drain().as_slice(), [xmtp_events::EventEnvelope { client: Some(ClientEvent::ArchiveRestored(restored)), .. }] if !restored.complete)
404 );
405 }
406 #[xmtp_common::test(unwrap_try = true)]
408 async fn archived_consent_keeps_the_latest_record() {
409 tester!(alix, disable_workers);
410 let records = [
411 (xmtp_db::consent_record::ConsentState::Allowed, 10),
412 (xmtp_db::consent_record::ConsentState::Allowed, 30),
413 (xmtp_db::consent_record::ConsentState::Denied, 20),
414 ];
415 let mut elements = futures::stream::iter(records.map(|(state, consented_at_ns)| {
416 let record = StoredConsentRecord {
417 entity_type: xmtp_db::consent_record::ConsentType::InboxId,
418 state,
419 entity: "archived-inbox".into(),
420 consented_at_ns,
421 };
422 Ok::<_, std::io::Error>(BackupElement {
423 element: Some(Element::Consent(record.into())),
424 })
425 }));
426 insert_elements(&mut elements, &alix.context).await?;
427 let stored = alix
428 .db()
429 .get_consent_record(
430 "archived-inbox".into(),
431 xmtp_db::consent_record::ConsentType::InboxId,
432 )?
433 .unwrap();
434 assert_eq!(stored.state, xmtp_db::consent_record::ConsentState::Allowed);
435 assert_eq!(stored.consented_at_ns, 30);
436 }
437
438 #[xmtp_common::test(unwrap_try = true)]
442 async fn archived_consent_time_move_reports_a_restore() {
443 tester!(alix, disable_workers);
444 let events = alix.context.events().subscribe(
445 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
446 Some(4),
447 );
448 let record = |consented_at_ns| StoredConsentRecord {
449 entity_type: xmtp_db::consent_record::ConsentType::InboxId,
450 state: xmtp_db::consent_record::ConsentState::Allowed,
451 entity: "time-move".into(),
452 consented_at_ns,
453 };
454 assert!(alix.db().insert_newer_consent_record(record(10))?.applied);
455 let mut elements = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
456 element: Some(Element::Consent(record(30).into())),
457 })]);
458 insert_elements(&mut elements, &alix.context).await?;
459 let stored = alix
460 .db()
461 .get_consent_record(
462 "time-move".into(),
463 xmtp_db::consent_record::ConsentType::InboxId,
464 )?
465 .unwrap();
466 assert_eq!(stored.consented_at_ns, 30);
467 assert!(matches!(
468 events.drain().as_slice(),
469 [xmtp_events::EventEnvelope {
470 client: Some(ClientEvent::ArchiveRestored(restored)), ..
471 }] if restored.complete
472 ));
473 }
474
475 #[xmtp_common::test(unwrap_try = true)]
476 async fn archive_timestamp_keeps_a_message_received_during_import() {
477 tester!(alix, disable_workers);
478 let group = alix.create_group(None, None)?;
479 let save = xmtp_proto::xmtp::device_sync::group_backup::GroupSave {
480 id: group.group_id.to_vec(),
481 last_message_ns: Some(0),
482 ..Default::default()
483 };
484
485 group.send_message_optimistic(b"message during import", Default::default())?;
486 let current = alix.db().find_group(&group.group_id)??.last_message_ns;
487 assert!(current > Some(0));
488 MlsGroup::restore_from_archive(&alix.context, &save)?;
489 assert_eq!(
490 alix.db().find_group(&group.group_id)??.last_message_ns,
491 current
492 );
493 }
494
495 #[xmtp_common::test(unwrap_try = true)]
496 async fn test_archive_timestamps() {
497 tester!(alix, disable_workers);
498 tester!(alix2, from: alix);
499 tester!(bo, disable_workers);
500
501 let alix_group = alix
502 .create_group_with_members(&[bo.inbox_id()], None, None)
503 .await?;
504 alix_group.send_message(b"hi", Default::default()).await?;
505
506 alix2.sync_welcomes().await?;
507 bo.sync_welcomes().await?;
508
509 let alix2_group = alix2.group(&alix_group.group_id)?;
510 let bo_group = bo.group(&alix_group.group_id)?;
511
512 alix2_group.sync().await?;
513 bo_group.sync().await?;
514
515 alix_group
517 .send_message(b"Hello again", Default::default())
518 .await?;
519
520 let key = vec![7; 32];
521 let opts = ArchiveOptions {
522 start_ns: None,
523 end_ns: None,
524 elements: vec![
525 BackupElementSelection::Messages,
526 BackupElementSelection::Consent,
527 ],
528 exclude_disappearing_messages: false,
529 };
530 let export = {
531 let mut file = vec![];
532 exporter::export(opts, alix.db(), &key, &mut file)?;
533 file
534 };
535
536 tester!(alix3, from: alix);
537
538 let reader = Box::pin(BufReader::new(Cursor::new(export.clone())));
541 let mut importer = ArchiveImporter::load(reader, &key).await?;
542 insert_importer(&mut importer, &alix2.context).await?;
543
544 let reader = Box::pin(BufReader::new(Cursor::new(export)));
545 let mut importer = ArchiveImporter::load(reader, &key).await?;
546 insert_importer(&mut importer, &alix3.context).await?;
547
548 let alix_timestamp = alix
549 .db()
550 .find_group(&alix_group.group_id)??
551 .last_message_ns?;
552 let alix2_timestamp = alix2
553 .db()
554 .find_group(&alix_group.group_id)??
555 .last_message_ns?;
556 let alix3_timestamp = alix3
557 .db()
558 .find_group(&alix_group.group_id)??
559 .last_message_ns?;
560
561 assert_eq!(alix2_timestamp, alix_timestamp);
563 assert_eq!(alix3_timestamp, alix_timestamp);
565 }
566
567 #[xmtp_common::test(unwrap_try = true)]
568 async fn test_dm_archive() {
569 tester!(alix, disable_workers);
570 tester!(bo, disable_workers);
571
572 let alix_bo_dm = alix.find_or_create_dm(bo.inbox_id(), None).await?;
573 let archived_message_id = alix_bo_dm
574 .send_message(b"old group", Default::default())
575 .await?;
576
577 let timestamp = alix
578 .db()
579 .find_group(&alix_bo_dm.group_id)??
580 .last_message_ns?;
581
582 let key = vec![7; 32];
583 let opts = ArchiveOptions {
584 start_ns: None,
585 end_ns: None,
586 elements: vec![
587 BackupElementSelection::Messages,
588 BackupElementSelection::Consent,
589 ],
590 exclude_disappearing_messages: false,
591 };
592 let export = {
593 let mut file = vec![];
594
595 exporter::export(opts, alix.db(), &key, &mut file)?;
596 file
597 };
598
599 tester!(alix2, from: alix);
600 let reader = Box::pin(BufReader::new(Cursor::new(export)));
601 let mut importer = ArchiveImporter::load(reader, &key).await?;
602 insert_importer(&mut importer, &alix2.context).await?;
603
604 let restored = alix2.group(&alix_bo_dm.group_id)?;
607 let restored_topic = xmtp_db::incoming_envelope::StreamTopic::group(alix_bo_dm.group_id);
608 let before_join = alix2.db().topic_progress(&restored_topic)?;
609 crate::mls_store::MlsStore::new(alix2.context.clone())
610 .receive_topics_once(
611 &[xmtp_proto::types::Topic::new_group_message(
612 alix_bo_dm.group_id,
613 )],
614 alix2
615 .context
616 .incoming_runtime()
617 .policy()
618 .incoming_limits(xmtp_db::incoming_envelope::NetworkEntityKind::Group),
619 )
620 .await?;
621 let received_before_join = alix2.db().topic_progress(&restored_topic)?;
622 assert!(received_before_join.received > before_join.processed);
623 assert!(matches!(
624 restored.process_pending_group_head(None)?,
625 crate::groups::mls_sync::GroupHeadOutcome::Inactive
626 ));
627 assert_eq!(
628 alix2.db().topic_progress(&restored_topic)?.processed,
629 before_join.processed
630 );
631 assert!(
632 !alix2
633 .db()
634 .pending_states_through(&restored_topic, received_before_join.received)?
635 .is_empty()
636 );
637
638 let alix2_bo_dm = alix2.find_or_create_dm(bo.inbox_id(), None).await?;
639 assert_ne!(alix_bo_dm.group_id, alix2_bo_dm.group_id);
640 let mut msgs = alix2_bo_dm.find_messages(&MsgQueryArgs::default())?;
641 assert_eq!(msgs.len(), 2);
642 assert!(
643 msgs.iter()
644 .any(|m| m.decrypted_message_bytes == b"old group")
645 );
646
647 let timestamp2 = alix2
650 .db()
651 .find_group(&alix_bo_dm.group_id)??
652 .last_message_ns?;
653 assert_eq!(timestamp, timestamp2);
654
655 let live_message_id = alix2_bo_dm
656 .send_message(b"hi bo", Default::default())
657 .await?;
658
659 bo.sync_all_welcomes_and_groups(None).await?;
660 let bo_alix2_dm = bo.group(&alix2_bo_dm.group_id)?;
661 assert_eq!(bo_alix2_dm.test_last_message_bytes().await??, b"hi bo");
662
663 alix2.sync_all_welcomes_and_groups(None).await?;
665 let rejoined_original = alix2.group(&alix_bo_dm.group_id)?;
666 assert!(rejoined_original.is_active()?);
667 let stitched = alix2_bo_dm.find_messages(&MsgQueryArgs::default())?;
668 assert_eq!(stitched.len(), 4);
669 let application_ids: Vec<_> = stitched
670 .iter()
671 .filter(|message| message.kind == xmtp_db::group_message::GroupMessageKind::Application)
672 .map(|message| message.id.clone())
673 .collect();
674 assert_eq!(application_ids, vec![archived_message_id, live_message_id]);
675 assert_eq!(alix2.find_groups(Default::default())?.len(), 1);
676 let bo_original = bo.group(&alix_bo_dm.group_id)?;
677 rejoined_original.test_can_talk_with(&bo_original).await?;
678 bo_original.test_can_talk_with(&rejoined_original).await?;
679 }
680
681 #[xmtp_common::test(unwrap_try = true)]
683 async fn foreign_archive_preserves_dm_pair_and_history() {
684 use crate::groups::GroupError;
685 use xmtp_db::{
686 consent_record::{ConsentState, ConsentType},
687 group::{DmIdExt, QueryGroup},
688 readd_status::ReaddStatus,
689 };
690 use xmtp_proto::api::HasStats;
691
692 tester!(alix, disable_workers);
693 tester!(bo, disable_workers);
694 tester!(charlie, disable_workers);
695 tester!(dana, disable_workers);
696 let source = alix.find_or_create_dm(bo.inbox_id(), None).await?;
697 let alix_id = source
698 .send_message(b"from alix", Default::default())
699 .await?;
700 bo.sync_welcomes().await?;
701 let bo_group = bo.group(&source.group_id)?;
702 let bo_id = bo_group
703 .send_message(b"from bo", Default::default())
704 .await?;
705 source.sync().await?;
706 let archived_dm_id = alix
707 .db()
708 .find_group(&source.group_id)?
709 .expect("source DM")
710 .dm_id
711 .expect("DM pair");
712 assert!(!archived_dm_id.contains(charlie.inbox_id()));
713
714 let key = vec![0x38; 32];
715 let opts = ArchiveOptions {
716 start_ns: None,
717 end_ns: None,
718 elements: vec![BackupElementSelection::Messages],
719 exclude_disappearing_messages: false,
720 };
721 let mut export = vec![];
722 exporter::export(opts.clone(), alix.db(), &key, &mut export)?;
723 let reader = Box::pin(BufReader::new(Cursor::new(export)));
724 let mut importer = ArchiveImporter::load(reader, &key).await?;
725 insert_importer(&mut importer, &charlie.context).await?;
726
727 let stored = charlie
728 .db()
729 .find_group(&source.group_id)?
730 .expect("Restored DM");
731 assert_eq!(stored.membership_state, GroupMembershipState::Restored);
732 assert_eq!(stored.dm_id.as_deref(), Some(archived_dm_id.as_str()));
733 assert_eq!(archived_dm_id.other_inbox_id(charlie.inbox_id()), None);
734 let restored = charlie.group(&source.group_id)?;
735 assert_eq!(
736 restored
737 .metadata()
738 .await?
739 .dm_members
740 .expect("stub pair")
741 .to_string(),
742 archived_dm_id
743 );
744 assert!(!restored.is_active()?);
745 let stats = charlie.context.api().api_client.mls_stats();
746 stats.clear();
747 assert!(matches!(
748 restored.send_message(b"inactive", Default::default()).await,
749 Err(GroupError::GroupInactive)
750 ));
751 assert!(matches!(
752 restored.sync().await,
753 Err(GroupError::GroupInactive)
754 ));
755 let Err(sync_summary) = restored.sync_with_conn().await else {
756 panic!("Restored sync_with_conn must reject the request");
757 };
758 assert!(matches!(
759 sync_summary.other.as_deref(),
760 Some(GroupError::GroupInactive)
761 ));
762 assert!(matches!(
763 restored.receive().await,
764 Err(GroupError::GroupInactive)
765 ));
766 assert!(matches!(
767 charlie.client.sync_all_groups(vec![restored.clone()]).await,
768 Err(GroupError::GroupInactive)
769 ));
770 assert_eq!(stats.publish.get_count(), 0);
771 let joined = charlie.create_group(None, None)?;
772 joined.add_members(&[dana.inbox_id()]).await?;
773 dana.sync_welcomes().await?;
774 let dana_group = dana.group(&joined.group_id)?;
775 let incoming_id = dana_group
776 .send_message(b"joined group", Default::default())
777 .await?;
778 let Err(GroupError::Sync(summary)) = charlie
779 .client
780 .sync_all_groups(vec![restored.clone(), joined])
781 .await
782 else {
783 panic!("mixed explicit sync must report the inactive group");
784 };
785 assert!(matches!(
786 summary.other.as_deref(),
787 Some(GroupError::GroupInactive)
788 ));
789 assert!(charlie.db().get_group_message(&incoming_id)?.is_some());
790 let sweep = charlie.client.sync_all_welcomes_and_groups(None).await?;
791 assert_eq!(sweep.num_eligible, 1);
792 assert_eq!(sweep.num_synced, 1);
793 assert!(!restored.is_active()?);
794 let db = charlie.db();
795 db.insert_or_replace_consent_records(&[StoredConsentRecord::new(
797 ConsentType::ConversationId,
798 ConsentState::Allowed,
799 hex::encode(source.group_id),
800 )])?;
801 let excludes_publish = db
802 .get_conversation_ids_for_remote_log_publish()?
803 .iter()
804 .all(|group| group.id != source.group_id);
805 let excludes_download = db
806 .get_conversation_ids_for_remote_log_download()?
807 .iter()
808 .all(|group| group.id != source.group_id);
809 let excludes_fork_check = db
810 .get_conversation_ids_for_fork_check()?
811 .iter()
812 .all(|id| id.as_slice() != source.group_id.as_ref());
813 db.set_group_commit_log_forked_status(&source.group_id, Some(true))?;
814 let excludes_requesting_readds = db
815 .get_conversation_ids_for_requesting_readds()?
816 .iter()
817 .all(|group| group.group_id != source.group_id);
818 ReaddStatus {
819 group_id: source.group_id,
820 installation_id: vec![0x42; 32],
821 requested_at_sequence_id: Some(1),
822 responded_at_sequence_id: None,
823 }
824 .store(&db)?;
825 let excludes_responding_readds = db
826 .get_conversation_ids_for_responding_readds()?
827 .iter()
828 .all(|group| group.group_id != source.group_id);
829 assert_eq!(
830 [
831 excludes_publish,
832 excludes_download,
833 excludes_fork_check,
834 excludes_requesting_readds,
835 excludes_responding_readds,
836 ],
837 [true; 5],
838 "Restored DM must be absent from every recovery selector"
839 );
840 for (id, sender, bytes) in [
841 (&alix_id, alix.inbox_id(), b"from alix".as_slice()),
842 (&bo_id, bo.inbox_id(), b"from bo".as_slice()),
843 ] {
844 let message = charlie.db().get_group_message(id)?.expect("pair history");
845 assert_eq!(message.sender_inbox_id, sender);
846 assert_eq!(message.decrypted_message_bytes, bytes);
847 }
848
849 let mut reexport = vec![];
850 exporter::export(opts, charlie.db(), &key, &mut reexport)?;
851 let reader = Box::pin(BufReader::new(Cursor::new(reexport)));
852 let mut exported = ArchiveImporter::load(reader, &key).await?;
853 while let Some(element) = exported.next().await {
854 match element?.element {
855 Some(Element::Group(group)) => assert_ne!(group.id, source.group_id.as_ref()),
856 Some(Element::GroupMessage(message)) => {
857 assert_ne!(message.group_id, source.group_id.as_ref());
858 }
859 _ => {}
860 }
861 }
862
863 let reopened = crate::builder::ClientBuilder::from_client(charlie.client.clone())
864 .with_disable_workers(true)
865 .build()
866 .await?;
867 let reopened_group = reopened.group(&source.group_id)?;
868 assert_eq!(
869 reopened_group.dm_id.as_deref(),
870 Some(archived_dm_id.as_str())
871 );
872 assert!(!reopened_group.is_active()?);
873 assert!(reopened.db().get_group_message(&alix_id)?.is_some());
874 assert!(reopened.db().get_group_message(&bo_id)?.is_some());
875 }
876
877 #[xmtp_common::test(unwrap_try = true)]
879 async fn archive_import_leaves_a_known_message_unchanged() {
880 tester!(alix, disable_workers);
881 let group = alix.create_group(None, None)?;
882 let known_id = group.send_message(b"known", Default::default()).await?;
883 let known = alix
884 .db()
885 .get_group_message(&known_id)?
886 .expect("known message");
887 let mut save: xmtp_proto::xmtp::device_sync::message_backup::GroupMessageSave =
888 known.clone().into();
889 save.sender_inbox_id = hex::encode([0x43; 32]);
890 save.decrypted_message_bytes = b"ignored replacement".to_vec();
891 let mut duplicate = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
892 element: Some(Element::GroupMessage(save)),
893 })]);
894 insert_elements(&mut duplicate, &alix.context).await?;
895 assert_eq!(alix.db().get_group_message(&known_id)?, Some(known));
896 }
897
898 #[xmtp_common::test(unwrap_try = true)]
903 async fn failed_import_keeps_archived_activity_of_restored_prefix() {
904 use xmtp_mls_common::group_metadata::DmMembers;
905 use xmtp_proto::xmtp::device_sync::{
906 group_backup::GroupSave,
907 message_backup::{GroupMessageKindSave, GroupMessageSave},
908 };
909
910 tester!(caro, disable_workers);
911 let pair = DmMembers {
912 member_one_inbox_id: hex::encode([0x41; 32]),
913 member_two_inbox_id: hex::encode([0x42; 32]),
914 };
915 let group_id = vec![0x65; 16];
916 let archived_activity = 1_234_567;
917 let group = GroupSave {
918 id: group_id.clone(),
919 conversation_type: 2,
920 dm_id: Some(pair.to_string()),
921 last_message_ns: Some(archived_activity),
922 ..Default::default()
923 };
924 let malformed = GroupMessageSave {
926 id: vec![0x66; 32],
927 group_id: group_id.clone(),
928 kind: GroupMessageKindSave::Unspecified as i32,
929 sender_inbox_id: pair.member_one_inbox_id.clone(),
930 ..Default::default()
931 };
932
933 let events = caro.context.events().subscribe(
934 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
935 Some(4),
936 );
937 let mut elements = futures::stream::iter([
938 Ok::<_, std::io::Error>(BackupElement {
939 element: Some(Element::Group(group)),
940 }),
941 Ok(BackupElement {
942 element: Some(Element::GroupMessage(malformed.clone())),
943 }),
944 ]);
945 let error = insert_elements(&mut elements, &caro.context)
946 .await
947 .expect_err("a malformed element fails the import");
948 assert!(
949 matches!(
950 error,
951 DeviceSyncError::ProtoConversion(xmtp_proto::ConversionError::Unspecified(
952 "message_kind"
953 ))
954 ),
955 "unexpected import error: {error:?}"
956 );
957
958 let restored = caro
959 .db()
960 .find_group(&GroupId::try_from(group_id.as_slice())?)??;
961 assert_eq!(restored.membership_state, GroupMembershipState::Restored);
962 assert_eq!(restored.last_message_ns, Some(archived_activity));
963 assert!(caro.db().get_group_message(&malformed.id)?.is_none());
964 assert!(matches!(
965 events.drain().as_slice(),
966 [xmtp_events::EventEnvelope {
967 client: Some(ClientEvent::ArchiveRestored(restored)), ..
968 }] if !restored.complete
969 ));
970 }
971
972 #[xmtp_common::test(unwrap_try = true)]
974 async fn malformed_archived_dm_id_fails_before_group_insert() {
975 tester!(alix, disable_workers);
976 let group_id = vec![0x64; 16];
977 let save = xmtp_proto::xmtp::device_sync::group_backup::GroupSave {
978 id: group_id.clone(),
979 conversation_type: 2,
980 dm_id: Some("dm:malformed".into()),
981 ..Default::default()
982 };
983 let mut elements = futures::stream::iter([Ok::<_, std::io::Error>(BackupElement {
984 element: Some(Element::Group(save)),
985 })]);
986 assert!(insert_elements(&mut elements, &alix.context).await.is_err());
987 assert!(
988 alix.db()
989 .find_group(&GroupId::try_from(group_id.as_slice())?)?
990 .is_none()
991 );
992 }
993
994 #[rstest::rstest]
996 #[xmtp_common::test]
997 async fn test_buffer_export_import() {
998 use futures::io::BufReader;
999 use futures_util::AsyncReadExt;
1000
1001 tester!(alix);
1002 tester!(bo);
1003
1004 let alix_group = alix.create_group(None, None).unwrap();
1005 alix_group.add_members(&[bo.inbox_id()]).await.unwrap();
1006 alix_group
1007 .send_message(b"hello there", SendMessageOpts::default())
1008 .await
1009 .unwrap();
1010
1011 let opts = ArchiveOptions {
1012 start_ns: None,
1013 end_ns: None,
1014 elements: vec![
1015 BackupElementSelection::Messages,
1016 BackupElementSelection::Consent,
1017 ],
1018 exclude_disappearing_messages: false,
1019 };
1020
1021 let key = vec![7; 32];
1022
1023 let file = {
1024 let mut file = Vec::new();
1025 exporter::export(opts, alix.db(), &key, &mut file).unwrap();
1026 file
1027 };
1028
1029 let alix2_wallet = generate_local_wallet();
1030 let alix2 = ClientBuilder::new_test_client(&alix2_wallet).await;
1031 let events = alix2.context.events().subscribe(
1032 xmtp_events::EventFilter::new([xmtp_events::EventKind::ArchiveRestored]),
1033 Some(4),
1034 );
1035
1036 let messages: Vec<StoredGroupMessage> = alix2
1038 .context
1039 .db()
1040 .raw_query(|conn| {
1041 group_messages::table
1042 .select(StoredGroupMessage::as_select())
1043 .load(conn)
1044 })
1045 .unwrap();
1046 assert_eq!(messages.len(), 0);
1047
1048 let reader = BufReader::new(Cursor::new(file.clone()));
1049 let reader = Box::pin(reader);
1050 let mut importer = ArchiveImporter::load(reader, &key).await.unwrap();
1051 insert_importer(&mut importer, &alix2.context)
1052 .await
1053 .unwrap();
1054 assert!(matches!(
1055 events.drain().as_slice(),
1056 [xmtp_events::EventEnvelope {
1057 client: Some(xmtp_events::ClientEvent::ArchiveRestored(restored)), ..
1058 }] if restored.complete
1059 ));
1060
1061 let reader = Box::pin(BufReader::new(Cursor::new(file)));
1062 let mut second = ArchiveImporter::load(reader, &key).await.unwrap();
1063 insert_importer(&mut second, &alix2.context).await.unwrap();
1064 assert!(events.drain().is_empty());
1065
1066 let messages: Vec<StoredGroupMessage> = alix2
1068 .context
1069 .db()
1070 .raw_query(|conn| {
1071 group_messages::table
1072 .select(StoredGroupMessage::as_select())
1073 .load(conn)
1074 })
1075 .unwrap();
1076 assert_eq!(messages.len(), 1);
1077 }
1078
1079 #[xmtp_common::test(unwrap_try = true)]
1081 #[cfg(not(target_arch = "wasm32"))]
1082 async fn test_file_backup() {
1083 use crate::{groups::send_message_opts::SendMessageOpts, tester};
1084 use diesel::QueryDsl;
1085 use xmtp_db::group::{ConversationType, GroupQueryArgs};
1086
1087 tester!(alix, sync_worker, triggers);
1088 tester!(bo);
1089
1090 let alix_group = alix.create_group(None, None)?;
1091
1092 wait_for_min_intents(&alix.context.db(), 2).await?;
1094
1095 alix_group.add_members(&[bo.inbox_id()]).await?;
1096 alix_group.update_group_name("My group".to_string()).await?;
1097
1098 bo.sync_welcomes().await?;
1099 let bo_group = bo.group(&alix_group.group_id)?;
1100
1101 wait_for_min_intents(&alix.context.db(), 1).await?;
1103
1104 alix_group
1105 .send_message(b"hello there", SendMessageOpts::default())
1106 .await?;
1107
1108 wait_for_min_intents(&alix.context.db(), 4).await?;
1111
1112 let mut consent_records: Vec<StoredConsentRecord> = alix
1113 .context
1114 .db()
1115 .raw_query(|conn| consent_records::table.load(conn))?;
1116 assert_eq!(consent_records.len(), 1);
1117 let old_consent_record = consent_records.pop()?;
1118
1119 let mut groups: Vec<StoredGroup> = alix
1120 .context
1121 .db()
1122 .raw_query(|conn| groups::table.load(conn))?;
1123 assert_eq!(groups.len(), 2);
1124 let old_group = groups.pop()?;
1125
1126 let old_messages: Vec<StoredGroupMessage> = alix.context.db().raw_query(|conn| {
1127 group_messages::table
1128 .select(StoredGroupMessage::as_select())
1129 .load(conn)
1130 })?;
1131 assert_eq!(old_messages.len(), 4);
1132
1133 let opts = ArchiveOptions {
1134 start_ns: None,
1135 end_ns: None,
1136 elements: vec![
1137 BackupElementSelection::Messages,
1138 BackupElementSelection::Consent,
1139 ],
1140 exclude_disappearing_messages: false,
1141 };
1142
1143 let key = xmtp_common::rand_vec::<32>();
1144 let path = Path::new("archive.xmtp");
1145 let _ = tokio::fs::remove_file(path).await;
1146 exporter::ArchiveExporter::export_to_file(opts, alix.db(), path, &key).await?;
1147
1148 tester!(alix2, sync_worker);
1149 alix2.device_sync_client().wait_for_sync_worker_init().await;
1150
1151 let consent_records: Vec<StoredConsentRecord> = alix2
1153 .context
1154 .db()
1155 .raw_query(|conn| consent_records::table.load(conn))?;
1156 assert_eq!(consent_records.len(), 0);
1157
1158 let mut importer = ArchiveImporter::from_file(path, &key).await?;
1159 insert_importer(&mut importer, &alix2.context)
1160 .await
1161 .unwrap();
1162
1163 let consent_records: Vec<StoredConsentRecord> = alix2
1165 .context
1166 .db()
1167 .raw_query(|conn| consent_records::table.load(conn))?;
1168 assert_eq!(consent_records.len(), 1);
1169 assert_eq!(consent_records[0], old_consent_record);
1171
1172 let groups: Vec<StoredGroup> = alix2.context.db().raw_query(|conn| {
1173 groups::table
1174 .filter(groups::conversation_type.ne_all(ConversationType::virtual_types()))
1175 .load(conn)
1176 })?;
1177 assert_eq!(groups.len(), 1);
1178 assert_eq!(groups[0].id, old_group.id);
1180
1181 let messages: Vec<StoredGroupMessage> = alix2.context.db().raw_query(|conn| {
1182 group_messages::table
1183 .select(StoredGroupMessage::as_select())
1184 .filter(group_messages::group_id.eq(&groups[0].id))
1185 .load(conn)
1186 })?;
1187 assert_eq!(messages.len(), 1);
1189 for msg in messages {
1190 let old_msg = old_messages.iter().find(|m| msg.id == m.id)?;
1191 assert_eq!(old_msg.authority_id, msg.authority_id);
1192 assert_eq!(old_msg.decrypted_message_bytes, msg.decrypted_message_bytes);
1193 assert_eq!(old_msg.sent_at_ns, msg.sent_at_ns);
1194 assert_eq!(old_msg.sender_installation_id, msg.sender_installation_id);
1195 assert_eq!(old_msg.sender_inbox_id, msg.sender_inbox_id);
1196 assert_eq!(old_msg.group_id, msg.group_id);
1197 }
1198
1199 let alix2_group = alix2.group(&old_group.id)?;
1200 let _groups = alix2.find_groups(GroupQueryArgs::default())?;
1202 alix2_group.group_name()?;
1204 assert!(!alix2_group.is_active()?);
1205
1206 alix.group(&old_group.id)?
1208 .add_members(&[alix2.inbox_id()])
1209 .await?;
1210 alix2.sync_welcomes().await?;
1211
1212 let alix2_group = alix2.group(&old_group.id)?;
1214 assert!(alix2_group.is_active()?);
1215
1216 let msgs = alix2_group.find_messages(&MsgQueryArgs::default())?;
1218 let old_msg_exists = msgs
1219 .iter()
1220 .any(|msg| msg.decrypted_message_bytes == b"hello there");
1221 assert!(old_msg_exists);
1222
1223 alix2_group
1225 .send_message(b"this should send", SendMessageOpts::default())
1226 .await?;
1227 bo_group.sync().await?;
1228 let msgs = bo_group.find_messages(&MsgQueryArgs::default())?;
1229 let new_msg_exists = msgs
1230 .iter()
1231 .any(|msg| msg.decrypted_message_bytes == b"this should send");
1232 assert!(new_msg_exists);
1233
1234 let _ = tokio::fs::remove_file(path).await;
1236 }
1237
1238 #[xmtp_common::test(unwrap_try = true)]
1239 #[cfg(not(target_arch = "wasm32"))]
1240 async fn test_legacy_archive_import() {
1241 use std::path::PathBuf;
1242
1243 use crate::tester;
1244
1245 let key = vec![0; 32];
1246 let path = PathBuf::from("tests/assets/archive-legacy.xmtp");
1247 let mut importer = ArchiveImporter::from_file(path, &key).await?;
1248
1249 tester!(alix);
1250
1251 let result = insert_importer(&mut importer, &alix.context).await;
1252 assert!(result.is_ok());
1253 }
1254
1255 #[xmtp_common::test(unwrap_try = true)]
1260 #[cfg(not(target_arch = "wasm32"))]
1261 async fn test_import_current_legacy_archive_metadata() {
1262 use std::path::PathBuf;
1263
1264 use crate::groups::group_permissions::PolicySet;
1265 use xmtp_db::group::ConversationType;
1266
1267 let key = vec![9; 32];
1268 let path = PathBuf::from("tests/assets/archive-current-legacy-metadata.xmtp");
1269 let mut preview = ArchiveImporter::from_file(path.clone(), &key).await?;
1270 let mut archived_dm_id = None;
1271 while let Some(element) = preview.next().await {
1272 if let Some(Element::Group(group)) = element?.element {
1273 archived_dm_id = archived_dm_id.or(group.dm_id);
1274 }
1275 }
1276 let archived_dm_id = archived_dm_id.expect("legacy DM pair");
1277 let mut importer = ArchiveImporter::from_file(path, &key).await?;
1278 tester!(alix, disable_workers);
1279 let pair = crate::groups::parse_canonical_dm_id(Some(&archived_dm_id))?;
1280 assert_ne!(pair.member_one_inbox_id, alix.inbox_id());
1281 assert_ne!(pair.member_two_inbox_id, alix.inbox_id());
1282 insert_importer(&mut importer, &alix.context).await?;
1283
1284 let groups: Vec<StoredGroup> = alix
1285 .context
1286 .db()
1287 .raw_query(|conn| groups::table.load(conn))?;
1288 assert_eq!(groups.len(), 2);
1289 for record in &groups {
1290 let group = alix.group(&record.id)?;
1291 group.with_group_snapshot(|mls| {
1292 assert!(mls.extensions().app_data_dictionary().is_some());
1293 Ok(())
1294 })?;
1295 }
1296
1297 let restored_group_record = groups
1298 .iter()
1299 .find(|group| group.conversation_type == ConversationType::Group)
1300 .expect("legacy group archive entry");
1301 assert_eq!(restored_group_record.message_disappear_from_ns, Some(123));
1302 assert_eq!(restored_group_record.message_disappear_in_ns, Some(456));
1303 let restored_group = alix.group(&restored_group_record.id)?;
1304 assert_eq!(restored_group.group_name()?, "legacy archive group name");
1305 assert_eq!(
1306 restored_group.group_description()?,
1307 "legacy archive group description"
1308 );
1309 assert_eq!(
1310 restored_group.group_image_url_square()?,
1311 "https://example.com/legacy-archive.png"
1312 );
1313 assert_eq!(restored_group.app_data()?, "legacy archive app data");
1314 assert_eq!(restored_group.permissions()?.policies, PolicySet::default());
1315
1316 let restored_dm_record = groups
1317 .iter()
1318 .find(|group| group.conversation_type == ConversationType::Dm)
1319 .expect("legacy DM archive entry");
1320 assert_eq!(
1321 restored_dm_record.dm_id.as_deref(),
1322 Some(archived_dm_id.as_str())
1323 );
1324 assert_eq!(restored_dm_record.message_disappear_from_ns, Some(123));
1325 assert_eq!(restored_dm_record.message_disappear_in_ns, Some(456));
1326 let restored_dm = alix.group(&restored_dm_record.id)?;
1327 assert_eq!(restored_dm.group_name()?, "legacy archive dm name");
1328 assert_eq!(
1329 restored_dm.group_description()?,
1330 "legacy archive dm description"
1331 );
1332 assert_eq!(
1333 restored_dm.group_image_url_square()?,
1334 "https://example.com/legacy-dm-archive.png"
1335 );
1336 assert_eq!(restored_dm.app_data()?, "legacy archive dm app data");
1337 }
1338
1339 #[xmtp_common::test(unwrap_try = true)]
1341 async fn test_archive_includes_dictionary_groups() {
1342 tester!(alix, disable_workers);
1343 tester!(bo, disable_workers);
1344
1345 let alix_group = alix
1346 .create_group_with_members(&[bo.inbox_id()], None, None)
1347 .await?;
1348 alix_group.send_message(b"hi", Default::default()).await?;
1349
1350 alix_group
1354 .update_group_name("post-migration name".to_string())
1355 .await?;
1356 alix_group
1357 .update_group_description("post-migration description".to_string())
1358 .await?;
1359 alix_group
1360 .update_group_image_url_square("https://example.com/post-migration.png".to_string())
1361 .await?;
1362
1363 let second_group = alix
1365 .create_group_with_members(&[bo.inbox_id()], None, None)
1366 .await?;
1367 second_group
1368 .update_group_name("second group name".to_string())
1369 .await?;
1370
1371 let key = vec![7; 32];
1372 let opts = ArchiveOptions {
1373 start_ns: None,
1374 end_ns: None,
1375 elements: vec![
1376 BackupElementSelection::Messages,
1377 BackupElementSelection::Consent,
1378 ],
1379 exclude_disappearing_messages: false,
1380 };
1381 let export = {
1382 let mut file = vec![];
1383 exporter::export(opts, alix.db(), &key, &mut file)?;
1384 file
1385 };
1386
1387 tester!(alix2, from: alix);
1390 let reader = Box::pin(BufReader::new(Cursor::new(export)));
1391 let mut importer = ArchiveImporter::load(reader, &key).await?;
1392 insert_importer(&mut importer, &alix2.context).await?;
1393
1394 let restored = alix2.db().find_group(&alix_group.group_id)?;
1395 assert!(
1396 restored.is_some(),
1397 "dictionary group missing from restored archive"
1398 );
1399
1400 let restored_group = alix2.group(&alix_group.group_id)?;
1404 assert_eq!(restored_group.group_name()?, "post-migration name");
1405 assert_eq!(
1406 restored_group.group_description()?,
1407 "post-migration description"
1408 );
1409 assert_eq!(
1410 restored_group.group_image_url_square()?,
1411 "https://example.com/post-migration.png"
1412 );
1413
1414 let restored_second = alix2.group(&second_group.group_id)?;
1415 assert_eq!(restored_second.group_name()?, "second group name");
1416 }
1417}
1418
1419#[cfg(test)]
1420mod restored_tests;
1421
1422#[cfg(all(test, not(target_arch = "wasm32")))]
1423mod migration_tests;