1use crate::{
6 client::CollectionMetadata, client::CollectionSignature,
7 schema::RemoteSettingsConnectionInitializer, Attachment, Error, RemoteSettingsRecord, Result,
8};
9use camino::Utf8PathBuf;
10use rusqlite::{params, Connection, OpenFlags, OptionalExtension, Transaction};
11use serde_json;
12use sha2::{Digest, Sha256};
13use std::io;
14
15use sql_support::{open_database::open_database_with_flags, ConnExt};
16
17pub struct Storage {
31 path: Utf8PathBuf,
32 conn: ConnectionCell,
33}
34
35impl Storage {
36 pub fn new(path: Utf8PathBuf) -> Self {
37 Self {
38 path,
39 conn: ConnectionCell::Uninitialized,
40 }
41 }
42
43 fn transaction(&mut self) -> Result<Transaction<'_>> {
44 match &self.conn {
45 ConnectionCell::Uninitialized => {
46 self.ensure_dir()?;
47 self.conn = ConnectionCell::Initialized(open_database_with_flags(
48 &self.path,
49 OpenFlags::default(),
50 &RemoteSettingsConnectionInitializer,
51 )?);
52 }
53 ConnectionCell::Initialized(_) => (),
54 ConnectionCell::Closed => return Err(Error::DatabaseClosed),
55 }
56 match &mut self.conn {
57 ConnectionCell::Initialized(conn) => Ok(conn.transaction()?),
58 _ => unreachable!(),
59 }
60 }
61
62 pub fn ensure_dir(&self) -> Result<()> {
63 if self.path == ":memory:" {
64 return Ok(());
65 }
66 let Some(dir) = self.path.parent() else {
67 return Ok(());
68 };
69 if !std::fs::exists(dir).map_err(Error::CreateDirError)? {
70 match std::fs::create_dir(dir) {
71 Ok(()) => (),
72 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => (),
74 Err(e) => return Err(Error::CreateDirError(e)),
75 }
76 }
77 Ok(())
78 }
79
80 pub fn close(&mut self) {
81 self.conn = ConnectionCell::Closed;
82 }
83
84 pub fn get_last_modified_timestamp(&mut self, collection_url: &str) -> Result<Option<u64>> {
89 let tx = self.transaction()?;
90 let mut stmt =
91 tx.prepare("SELECT last_modified FROM collection_metadata WHERE collection_url = ?")?;
92 let result: Option<u64> = stmt
93 .query_row((collection_url,), |row| row.get(0))
94 .optional()?;
95 Ok(result)
96 }
97
98 pub fn get_records(
103 &mut self,
104 collection_url: &str,
105 ) -> Result<Option<Vec<RemoteSettingsRecord>>> {
106 let tx = self.transaction()?;
107
108 let fetched = tx.exists(
109 "SELECT 1 FROM collection_metadata WHERE collection_url = ?",
110 (collection_url,),
111 )?;
112 let result = if fetched {
113 let records: Vec<RemoteSettingsRecord> = tx
115 .prepare("SELECT data FROM records WHERE collection_url = ?")?
116 .query_map(params![collection_url], |row| row.get::<_, Vec<u8>>(0))?
117 .map(|data| serde_json::from_slice(&data.unwrap()).unwrap())
118 .collect();
119
120 Ok(Some(records))
121 } else {
122 Ok(None)
123 };
124
125 tx.commit()?;
126 result
127 }
128
129 pub fn get_collection_metadata(
134 &mut self,
135 collection_url: &str,
136 ) -> Result<Option<CollectionMetadata>> {
137 let tx = self.transaction()?;
138 let mut stmt_metadata = tx.prepare(
142 "
143 SELECT
144 cm.bucket,
145 json_extract(sig.value, '$.x5u') AS x5u,
146 json_extract(sig.value, '$.signature') AS signature
147 FROM collection_metadata AS cm
148 LEFT JOIN json_each(cm.signatures) AS sig ON true
149 WHERE cm.collection_url = ?
150 ",
151 )?;
152
153 let mut rows = stmt_metadata.query(params![collection_url])?;
154 let mut bucket: Option<String> = None;
155 let mut signatures = Vec::new();
156
157 while let Some(row) = rows.next()? {
158 if bucket.is_none() {
160 bucket = Some(row.get(0)?);
161 }
162 let x5u: Option<String> = row.get(1)?;
163 let signature: Option<String> = row.get(2)?;
164 if let (Some(x5u), Some(signature)) = (x5u, signature) {
165 signatures.push(CollectionSignature { signature, x5u });
166 }
167 }
168 match bucket {
169 Some(bucket) => Ok(Some(CollectionMetadata { bucket, signatures })),
170 None => Ok(None),
171 }
172 }
173
174 pub fn get_attachment(
181 &mut self,
182 collection_url: &str,
183 metadata: Attachment,
184 ) -> Result<Option<Vec<u8>>> {
185 let tx = self.transaction()?;
186 let mut stmt =
187 tx.prepare("SELECT data FROM attachments WHERE id = ? AND collection_url = ?")?;
188
189 if let Some(data) = stmt
190 .query_row((metadata.location, collection_url), |row| {
191 row.get::<_, Vec<u8>>(0)
192 })
193 .optional()?
194 {
195 if data.len() as u64 != metadata.size {
197 return Ok(None);
198 }
199 let hash = format!("{:x}", Sha256::digest(&data));
200 if hash != metadata.hash {
201 return Ok(None);
202 }
203 Ok(Some(data))
204 } else {
205 Ok(None)
206 }
207 }
208
209 pub fn insert_collection_content(
211 &mut self,
212 collection_url: &str,
213 records: &[RemoteSettingsRecord],
214 last_modified: u64,
215 metadata: CollectionMetadata,
216 ) -> Result<()> {
217 let tx = self.transaction()?;
218
219 tx.execute(
224 "DELETE FROM records where collection_url <> ?",
225 [collection_url],
226 )?;
227 tx.execute(
228 "DELETE FROM collection_metadata where collection_url <> ?",
229 [collection_url],
230 )?;
231
232 Self::update_record_rows(&tx, collection_url, records)?;
233 Self::update_collection_metadata(&tx, collection_url, last_modified, metadata)?;
234 Self::cleanup_orphaned_attachments(&tx, collection_url)?;
235 tx.commit()?;
236 Ok(())
237 }
238
239 fn update_record_rows(
243 tx: &Transaction<'_>,
244 collection_url: &str,
245 records: &[RemoteSettingsRecord],
246 ) -> Result<u64> {
247 let mut max_last_modified = 0;
249 {
250 let mut insert_stmt = tx.prepare(
251 "INSERT OR REPLACE INTO records (id, collection_url, data) VALUES (?, ?, ?)",
252 )?;
253 let mut delete_stmt = tx.prepare("DELETE FROM records WHERE id=?")?;
254 for record in records {
255 if record.deleted {
256 delete_stmt.execute(params![&record.id])?;
257 } else {
258 max_last_modified = max_last_modified.max(record.last_modified);
259 let data = serde_json::to_vec(&record)?;
260 insert_stmt.execute(params![record.id, collection_url, data])?;
261 }
262 }
263 }
264 Ok(max_last_modified)
265 }
266
267 fn update_collection_metadata(
269 tx: &Transaction<'_>,
270 collection_url: &str,
271 last_modified: u64,
272 metadata: CollectionMetadata,
273 ) -> Result<()> {
274 let signatures_json = serde_json::to_string(&metadata.signatures)
275 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
276
277 let mut stmt = tx.prepare(
278 "INSERT OR REPLACE INTO collection_metadata
279 (collection_url, last_modified, bucket, signatures)
280 VALUES (?, ?, ?, ?)",
281 )?;
282
283 stmt.execute((
284 collection_url,
285 last_modified,
286 &metadata.bucket,
287 &signatures_json,
288 ))?;
289 Ok(())
290 }
291
292 pub fn set_attachment(
294 &mut self,
295 collection_url: &str,
296 location: &str,
297 attachment: &[u8],
298 ) -> Result<()> {
299 let tx = self.transaction()?;
300
301 tx.execute(
303 "DELETE FROM attachments WHERE collection_url != ?",
304 params![collection_url],
305 )?;
306
307 tx.execute(
308 "INSERT OR REPLACE INTO ATTACHMENTS \
309 (id, collection_url, data) \
310 VALUES (?, ?, ?)",
311 params![location, collection_url, attachment,],
312 )?;
313
314 tx.commit()?;
315
316 Ok(())
317 }
318
319 pub fn empty(&mut self) -> Result<()> {
323 let tx = self.transaction()?;
324 tx.execute("DELETE FROM records", [])?;
325 tx.execute("DELETE FROM attachments", [])?;
326 tx.execute("DELETE FROM collection_metadata", [])?;
327 tx.commit()?;
328 Ok(())
329 }
330
331 fn cleanup_orphaned_attachments(tx: &Transaction<'_>, collection_url: &str) -> Result<()> {
336 tx.execute(
337 "DELETE FROM attachments
338 WHERE collection_url = ?1
339 AND NOT EXISTS (
340 SELECT 1 FROM records WHERE collection_url = ?1
341 AND json_extract(data, '$.attachment.location') = attachments.id
342 )",
343 params![collection_url],
344 )?;
345 Ok(())
346 }
347}
348
349enum ConnectionCell {
351 Uninitialized,
352 Initialized(Connection),
353 Closed,
354}
355
356#[cfg(test)]
357mod tests {
358 use super::Storage;
359 use crate::{
360 client::CollectionMetadata, client::CollectionSignature, Attachment, RemoteSettingsRecord,
361 Result, RsJsonObject,
362 };
363 use sha2::{Digest, Sha256};
364
365 #[test]
366 fn test_storage_set_and_get_records() -> Result<()> {
367 let mut storage = Storage::new(":memory:".into());
368
369 let collection_url = "https://example.com/api";
370 let records = vec![
371 RemoteSettingsRecord {
372 id: "1".to_string(),
373 last_modified: 100,
374 deleted: false,
375 attachment: None,
376 fields: serde_json::json!({"key": "value1"})
377 .as_object()
378 .unwrap()
379 .clone(),
380 },
381 RemoteSettingsRecord {
382 id: "2".to_string(),
383 last_modified: 200,
384 deleted: false,
385 attachment: None,
386 fields: serde_json::json!({"key": "value2"})
387 .as_object()
388 .unwrap()
389 .clone(),
390 },
391 ];
392
393 storage.insert_collection_content(
395 collection_url,
396 &records,
397 300,
398 CollectionMetadata::default(),
399 )?;
400
401 let fetched_records = storage.get_records(collection_url)?;
403 assert!(fetched_records.is_some());
404 let fetched_records = fetched_records.unwrap();
405 assert_eq!(fetched_records.len(), 2);
406 assert_eq!(fetched_records, records);
407
408 assert_eq!(fetched_records[0].fields["key"], "value1");
409
410 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
412 assert_eq!(last_modified, Some(300));
413
414 Ok(())
415 }
416
417 #[test]
418 fn test_storage_get_records_none() -> Result<()> {
419 let mut storage = Storage::new(":memory:".into());
420
421 let collection_url = "https://example.com/api";
422
423 let fetched_records = storage.get_records(collection_url)?;
425 assert!(fetched_records.is_none());
426
427 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
429 assert!(last_modified.is_none());
430
431 Ok(())
432 }
433
434 #[test]
435 fn test_storage_get_records_empty() -> Result<()> {
436 let mut storage = Storage::new(":memory:".into());
437
438 let collection_url = "https://example.com/api";
439
440 storage.insert_collection_content(
442 collection_url,
443 &Vec::<RemoteSettingsRecord>::default(),
444 42,
445 CollectionMetadata::default(),
446 )?;
447
448 let fetched_records = storage.get_records(collection_url)?;
450 assert_eq!(fetched_records, Some(Vec::new()));
451
452 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
454 assert_eq!(last_modified, Some(42));
455
456 Ok(())
457 }
458
459 #[test]
460 fn test_storage_set_and_get_attachment() -> Result<()> {
461 let mut storage = Storage::new(":memory:".into());
462
463 let attachment = &[0x18, 0x64];
464 let collection_url = "https://example.com/api";
465 let attachment_metadata = Attachment {
466 filename: "abc".to_string(),
467 mimetype: "application/json".to_string(),
468 location: "tmp".to_string(),
469 hash: format!("{:x}", Sha256::digest(attachment)),
470 size: attachment.len() as u64,
471 };
472
473 storage.set_attachment(collection_url, &attachment_metadata.location, attachment)?;
475
476 let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata)?;
478 assert!(fetched_attachment.is_some());
479 let fetched_attachment = fetched_attachment.unwrap();
480 assert_eq!(fetched_attachment, attachment);
481
482 Ok(())
483 }
484
485 #[test]
486 fn test_storage_set_and_replace_attachment() -> Result<()> {
487 let mut storage = Storage::new(":memory:".into());
488
489 let collection_url = "https://example.com/api";
490
491 let attachment_1 = &[0x18, 0x64];
492 let attachment_2 = &[0x12, 0x48];
493
494 let attachment_metadata_1 = Attachment {
495 filename: "abc".to_string(),
496 mimetype: "application/json".to_string(),
497 location: "tmp".to_string(),
498 hash: format!("{:x}", Sha256::digest(attachment_1)),
499 size: attachment_1.len() as u64,
500 };
501
502 let attachment_metadata_2 = Attachment {
503 filename: "def".to_string(),
504 mimetype: "application/json".to_string(),
505 location: "tmp".to_string(),
506 hash: format!("{:x}", Sha256::digest(attachment_2)),
507 size: attachment_2.len() as u64,
508 };
509
510 storage.set_attachment(
512 collection_url,
513 &attachment_metadata_1.location,
514 attachment_1,
515 )?;
516
517 storage.set_attachment(
519 collection_url,
520 &attachment_metadata_2.location,
521 attachment_2,
522 )?;
523
524 let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata_2)?;
526 assert!(fetched_attachment.is_some());
527 let fetched_attachment = fetched_attachment.unwrap();
528 assert_eq!(fetched_attachment, attachment_2);
529
530 Ok(())
531 }
532
533 #[test]
534 fn test_storage_set_attachment_delete_others() -> Result<()> {
535 let mut storage = Storage::new(":memory:".into());
536
537 let collection_url_1 = "https://example.com/api1";
538 let collection_url_2 = "https://example.com/api2";
539
540 let attachment_1 = &[0x18, 0x64];
541 let attachment_2 = &[0x12, 0x48];
542
543 let attachment_metadata_1 = Attachment {
544 filename: "abc".to_string(),
545 mimetype: "application/json".to_string(),
546 location: "first_tmp".to_string(),
547 hash: format!("{:x}", Sha256::digest(attachment_1)),
548 size: attachment_1.len() as u64,
549 };
550
551 let attachment_metadata_2 = Attachment {
552 filename: "def".to_string(),
553 mimetype: "application/json".to_string(),
554 location: "second_tmp".to_string(),
555 hash: format!("{:x}", Sha256::digest(attachment_2)),
556 size: attachment_2.len() as u64,
557 };
558
559 storage.set_attachment(
561 collection_url_1,
562 &attachment_metadata_1.location,
563 attachment_1,
564 )?;
565 storage.set_attachment(
566 collection_url_2,
567 &attachment_metadata_2.location,
568 attachment_2,
569 )?;
570
571 let fetched_attachment_1 =
573 storage.get_attachment(collection_url_1, attachment_metadata_1)?;
574 assert!(fetched_attachment_1.is_none());
575
576 let fetched_attachment_2 =
577 storage.get_attachment(collection_url_2, attachment_metadata_2)?;
578 assert!(fetched_attachment_2.is_some());
579 let fetched_attachment_2 = fetched_attachment_2.unwrap();
580 assert_eq!(fetched_attachment_2, attachment_2);
581
582 Ok(())
583 }
584
585 #[test]
599 fn test_storage_orphaned_attachments_cleaned_up_on_update() -> Result<()> {
600 let mut storage = Storage::new(":memory:".into());
601 let collection_url = "https://example.com/api";
602
603 let attachment_v1 = b"version 1 data";
604 let attachment_v2 = b"version 2 data";
605
606 let attachment_meta_v1 = Attachment {
607 filename: "sponsored-suggestions-us-phone.json".to_string(),
608 mimetype: "application/json".to_string(),
609 location: "main-workspace/quicksuggest-amp/attachment-v1.json".to_string(),
610 hash: format!("{:x}", Sha256::digest(attachment_v1)),
611 size: attachment_v1.len() as u64,
612 };
613
614 let attachment_meta_v2 = Attachment {
615 filename: "sponsored-suggestions-us-phone.json".to_string(),
616 mimetype: "application/json".to_string(),
617 location: "main-workspace/quicksuggest-amp/attachment-v2.json".to_string(),
618 hash: format!("{:x}", Sha256::digest(attachment_v2)),
619 size: attachment_v2.len() as u64,
620 };
621
622 let records_v1 = vec![RemoteSettingsRecord {
624 id: "sponsored-suggestions-us-phone".to_string(),
625 last_modified: 100,
626 deleted: false,
627 attachment: Some(attachment_meta_v1.clone()),
628 fields: serde_json::json!({"type": "amp"})
629 .as_object()
630 .unwrap()
631 .clone(),
632 }];
633
634 storage.insert_collection_content(
635 collection_url,
636 &records_v1,
637 100,
638 CollectionMetadata::default(),
639 )?;
640 storage.set_attachment(collection_url, &attachment_meta_v1.location, attachment_v1)?;
641
642 let fetched = storage.get_attachment(collection_url, attachment_meta_v1.clone())?;
643 assert!(fetched.is_some(), "v1 attachment should be stored");
644
645 let records_v2 = vec![RemoteSettingsRecord {
648 id: "sponsored-suggestions-us-phone".to_string(),
649 last_modified: 200,
650 deleted: false,
651 attachment: Some(attachment_meta_v2.clone()),
652 fields: serde_json::json!({"type": "amp"})
653 .as_object()
654 .unwrap()
655 .clone(),
656 }];
657
658 storage.insert_collection_content(
659 collection_url,
660 &records_v2,
661 200,
662 CollectionMetadata::default(),
663 )?;
664 storage.set_attachment(collection_url, &attachment_meta_v2.location, attachment_v2)?;
665
666 let fetched_v2 = storage.get_attachment(collection_url, attachment_meta_v2)?;
667 assert!(fetched_v2.is_some(), "v2 attachment should be stored");
668
669 let fetched_v1 = storage.get_attachment(collection_url, attachment_meta_v1)?;
670 assert!(
671 fetched_v1.is_none(),
672 "v1 attachment should be cleaned up after record points to v2"
673 );
674
675 Ok(())
676 }
677
678 #[test]
689 fn test_storage_orphaned_attachments_cleaned_up_on_delete() -> Result<()> {
690 let mut storage = Storage::new(":memory:".into());
691 let collection_url = "https://example.com/api";
692
693 let attachment_data = b"sponsored suggestions for GB phone";
694
695 let attachment_meta = Attachment {
696 filename: "sponsored-suggestions-gb-phone.json".to_string(),
697 mimetype: "application/json".to_string(),
698 location: "main-workspace/quicksuggest-amp/attachment-gb.json".to_string(),
699 hash: format!("{:x}", Sha256::digest(attachment_data)),
700 size: attachment_data.len() as u64,
701 };
702
703 let initial_records = vec![
705 RemoteSettingsRecord {
706 id: "sponsored-suggestions-gb-phone".to_string(),
707 last_modified: 100,
708 deleted: false,
709 attachment: Some(attachment_meta.clone()),
710 fields: serde_json::json!({"type": "amp"})
711 .as_object()
712 .unwrap()
713 .clone(),
714 },
715 RemoteSettingsRecord {
716 id: "sponsored-suggestions-us-phone".to_string(),
717 last_modified: 100,
718 deleted: false,
719 attachment: None,
720 fields: serde_json::json!({"type": "amp"})
721 .as_object()
722 .unwrap()
723 .clone(),
724 },
725 ];
726
727 storage.insert_collection_content(
728 collection_url,
729 &initial_records,
730 100,
731 CollectionMetadata::default(),
732 )?;
733 storage.set_attachment(collection_url, &attachment_meta.location, attachment_data)?;
734
735 let fetched = storage.get_attachment(collection_url, attachment_meta.clone())?;
736 assert!(fetched.is_some(), "GB attachment should be stored");
737
738 let updated_records = vec![RemoteSettingsRecord {
740 id: "sponsored-suggestions-gb-phone".to_string(),
741 last_modified: 200,
742 deleted: true,
743 attachment: None,
744 fields: RsJsonObject::new(),
745 }];
746
747 storage.insert_collection_content(
748 collection_url,
749 &updated_records,
750 200,
751 CollectionMetadata::default(),
752 )?;
753
754 let fetched = storage.get_attachment(collection_url, attachment_meta)?;
755 assert!(
756 fetched.is_none(),
757 "GB attachment should be cleaned up after record is deleted via tombstone"
758 );
759
760 Ok(())
761 }
762
763 #[test]
764 fn test_storage_get_attachment_not_found() -> Result<()> {
765 let mut storage = Storage::new(":memory:".into());
766
767 let collection_url = "https://example.com/api";
768 let metadata = Attachment::default();
769
770 let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
772 assert!(fetched_attachment.is_none());
773
774 Ok(())
775 }
776
777 #[test]
778 fn test_storage_empty() -> Result<()> {
779 let mut storage = Storage::new(":memory:".into());
780
781 let collection_url = "https://example.com/api";
782 let attachment = &[0x18, 0x64];
783
784 let records = vec![
785 RemoteSettingsRecord {
786 id: "1".to_string(),
787 last_modified: 100,
788 deleted: false,
789 attachment: None,
790 fields: serde_json::json!({"key": "value1"})
791 .as_object()
792 .unwrap()
793 .clone(),
794 },
795 RemoteSettingsRecord {
796 id: "2".to_string(),
797 last_modified: 200,
798 deleted: false,
799 attachment: Some(Attachment {
800 filename: "abc".to_string(),
801 mimetype: "application/json".to_string(),
802 location: "tmp".to_string(),
803 hash: format!("{:x}", Sha256::digest(attachment)),
804 size: attachment.len() as u64,
805 }),
806 fields: serde_json::json!({"key": "value2"})
807 .as_object()
808 .unwrap()
809 .clone(),
810 },
811 ];
812
813 let metadata = records[1]
814 .clone()
815 .attachment
816 .expect("No attachment metadata for record");
817
818 storage.insert_collection_content(
820 collection_url,
821 &records,
822 42,
823 CollectionMetadata::default(),
824 )?;
825 storage.set_attachment(collection_url, &metadata.location, attachment)?;
826
827 let fetched_records = storage.get_records(collection_url)?;
829 assert!(fetched_records.is_some());
830 let fetched_attachment = storage.get_attachment(collection_url, metadata.clone())?;
831 assert!(fetched_attachment.is_some());
832
833 storage.empty()?;
835
836 let fetched_records = storage.get_records(collection_url)?;
838 assert!(fetched_records.is_none());
839 let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
840 assert!(fetched_attachment.is_none());
841
842 Ok(())
843 }
844
845 #[test]
846 fn test_storage_collection_url_isolation() -> Result<()> {
847 let mut storage = Storage::new(":memory:".into());
848
849 let collection_url1 = "https://example.com/api1";
850 let collection_url2 = "https://example.com/api2";
851 let records_collection_url1 = vec![RemoteSettingsRecord {
852 id: "1".to_string(),
853 last_modified: 100,
854 deleted: false,
855 attachment: None,
856 fields: serde_json::json!({"key": "value1"})
857 .as_object()
858 .unwrap()
859 .clone(),
860 }];
861 let records_collection_url2 = vec![RemoteSettingsRecord {
862 id: "2".to_string(),
863 last_modified: 200,
864 deleted: false,
865 attachment: None,
866 fields: serde_json::json!({"key": "value2"})
867 .as_object()
868 .unwrap()
869 .clone(),
870 }];
871
872 storage.insert_collection_content(
874 collection_url1,
875 &records_collection_url1,
876 42,
877 CollectionMetadata::default(),
878 )?;
879 let fetched_records = storage.get_records(collection_url1)?;
881 assert!(fetched_records.is_some());
882 let fetched_records = fetched_records.unwrap();
883 assert_eq!(fetched_records.len(), 1);
884 assert_eq!(fetched_records, records_collection_url1);
885
886 storage.insert_collection_content(
888 collection_url2,
889 &records_collection_url2,
890 300,
891 CollectionMetadata::default(),
892 )?;
893
894 let fetched_records = storage.get_records(collection_url1)?;
896 assert!(fetched_records.is_none());
897
898 let fetched_records = storage.get_records(collection_url2)?;
900 assert!(fetched_records.is_some());
901 let fetched_records = fetched_records.unwrap();
902 assert_eq!(fetched_records.len(), 1);
903 assert_eq!(fetched_records, records_collection_url2);
904
905 let last_modified1 = storage.get_last_modified_timestamp(collection_url1)?;
907 assert_eq!(last_modified1, None);
908 let last_modified2 = storage.get_last_modified_timestamp(collection_url2)?;
909 assert_eq!(last_modified2, Some(300));
910
911 Ok(())
912 }
913
914 #[test]
915 fn test_storage_insert_collection_content() -> Result<()> {
916 let mut storage = Storage::new(":memory:".into());
917
918 let collection_url = "https://example.com/api";
919 let initial_records = vec![RemoteSettingsRecord {
920 id: "2".to_string(),
921 last_modified: 200,
922 deleted: false,
923 attachment: None,
924 fields: serde_json::json!({"key": "value2"})
925 .as_object()
926 .unwrap()
927 .clone(),
928 }];
929
930 storage.insert_collection_content(
932 collection_url,
933 &initial_records,
934 42,
935 CollectionMetadata::default(),
936 )?;
937
938 let fetched_records = storage.get_records(collection_url)?;
940 assert!(fetched_records.is_some());
941 assert_eq!(fetched_records.unwrap(), initial_records);
942
943 let updated_records = vec![RemoteSettingsRecord {
945 id: "2".to_string(),
946 last_modified: 200,
947 deleted: false,
948 attachment: None,
949 fields: serde_json::json!({"key": "value2_updated"})
950 .as_object()
951 .unwrap()
952 .clone(),
953 }];
954 storage.insert_collection_content(
955 collection_url,
956 &updated_records,
957 300,
958 CollectionMetadata::default(),
959 )?;
960
961 let fetched_records = storage.get_records(collection_url)?;
963 assert!(fetched_records.is_some());
964 assert_eq!(fetched_records.unwrap(), updated_records);
965
966 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
968 assert_eq!(last_modified, Some(300));
969
970 Ok(())
971 }
972
973 fn test_fields(data: &str) -> RsJsonObject {
975 let mut map = serde_json::Map::new();
976 map.insert("data".into(), data.into());
977 map
978 }
979
980 #[test]
981 fn test_storage_merge_records() -> Result<()> {
982 let mut storage = Storage::new(":memory:".into());
983
984 let collection_url = "https://example.com/api";
985
986 let initial_records = vec![
987 RemoteSettingsRecord {
988 id: "a".into(),
989 last_modified: 100,
990 deleted: false,
991 attachment: None,
992 fields: test_fields("a"),
993 },
994 RemoteSettingsRecord {
995 id: "b".into(),
996 last_modified: 200,
997 deleted: false,
998 attachment: None,
999 fields: test_fields("b"),
1000 },
1001 RemoteSettingsRecord {
1002 id: "c".into(),
1003 last_modified: 300,
1004 deleted: false,
1005 attachment: None,
1006 fields: test_fields("c"),
1007 },
1008 ];
1009 let updated_records = vec![
1010 RemoteSettingsRecord {
1012 id: "d".into(),
1013 last_modified: 1300,
1014 deleted: false,
1015 attachment: None,
1016 fields: test_fields("d"),
1017 },
1018 RemoteSettingsRecord {
1020 id: "b".into(),
1021 last_modified: 1200,
1022 deleted: true,
1023 attachment: None,
1024 fields: RsJsonObject::new(),
1025 },
1026 RemoteSettingsRecord {
1028 id: "a".into(),
1029 last_modified: 1100,
1030 deleted: false,
1031 attachment: None,
1032 fields: test_fields("a-with-new-data"),
1033 },
1034 ];
1036 let expected_records = vec![
1037 RemoteSettingsRecord {
1039 id: "a".into(),
1040 last_modified: 1100,
1041 deleted: false,
1042 attachment: None,
1043 fields: test_fields("a-with-new-data"),
1044 },
1045 RemoteSettingsRecord {
1046 id: "c".into(),
1047 last_modified: 300,
1048 deleted: false,
1049 attachment: None,
1050 fields: test_fields("c"),
1051 },
1052 RemoteSettingsRecord {
1053 id: "d".into(),
1054 last_modified: 1300,
1055 deleted: false,
1056 attachment: None,
1057 fields: test_fields("d"),
1058 },
1059 ];
1060
1061 storage.insert_collection_content(
1063 collection_url,
1064 &initial_records,
1065 1000,
1066 CollectionMetadata::default(),
1067 )?;
1068
1069 let fetched_records = storage.get_records(collection_url)?.unwrap();
1071 assert_eq!(fetched_records, initial_records);
1072
1073 storage.insert_collection_content(
1075 collection_url,
1076 &updated_records,
1077 1300,
1078 CollectionMetadata::default(),
1079 )?;
1080
1081 let mut fetched_records = storage.get_records(collection_url)?.unwrap();
1083 fetched_records.sort_by_cached_key(|r| r.id.clone());
1084 assert_eq!(fetched_records, expected_records);
1085
1086 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
1088 assert_eq!(last_modified, Some(1300));
1089 Ok(())
1090 }
1091 #[test]
1092 fn test_storage_get_collection_metadata() -> Result<()> {
1093 let mut storage = Storage::new(":memory:".into());
1094
1095 let collection_url = "https://example.com/api";
1096 let initial_records = vec![RemoteSettingsRecord {
1097 id: "2".to_string(),
1098 last_modified: 200,
1099 deleted: false,
1100 attachment: None,
1101 fields: serde_json::json!({"key": "value2"})
1102 .as_object()
1103 .unwrap()
1104 .clone(),
1105 }];
1106
1107 storage.insert_collection_content(
1109 collection_url,
1110 &initial_records,
1111 1337,
1112 CollectionMetadata {
1113 bucket: "main".into(),
1114 signatures: vec![
1115 CollectionSignature {
1116 signature: "b64encodedsig".into(),
1117 x5u: "http://15u/".into(),
1118 },
1119 CollectionSignature {
1120 signature: "b64encodedsig2".into(),
1121 x5u: "http://15u2/".into(),
1122 },
1123 ],
1124 },
1125 )?;
1126
1127 let metadata = storage.get_collection_metadata(collection_url)?.unwrap();
1128
1129 assert_eq!(metadata.signatures[0].signature, "b64encodedsig");
1130 assert_eq!(metadata.signatures[0].x5u, "http://15u/");
1131 assert_eq!(metadata.signatures[1].signature, "b64encodedsig2");
1132 assert_eq!(metadata.signatures[1].x5u, "http://15u2/");
1133
1134 Ok(())
1135 }
1136}