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, run_maintenance, 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 pub fn run_maintenance(&mut self) -> Result<()> {
349 if let ConnectionCell::Initialized(conn) = &self.conn {
350 run_maintenance(conn)?;
351 }
352
353 Ok(())
354 }
355}
356
357enum ConnectionCell {
359 Uninitialized,
360 Initialized(Connection),
361 Closed,
362}
363
364#[cfg(test)]
365mod tests {
366 use super::Storage;
367 use crate::{
368 client::CollectionMetadata, client::CollectionSignature, Attachment, RemoteSettingsRecord,
369 Result, RsJsonObject,
370 };
371 use sha2::{Digest, Sha256};
372
373 #[test]
374 fn test_storage_set_and_get_records() -> Result<()> {
375 let mut storage = Storage::new(":memory:".into());
376
377 let collection_url = "https://example.com/api";
378 let records = vec![
379 RemoteSettingsRecord {
380 id: "1".to_string(),
381 last_modified: 100,
382 deleted: false,
383 attachment: None,
384 fields: serde_json::json!({"key": "value1"})
385 .as_object()
386 .unwrap()
387 .clone(),
388 },
389 RemoteSettingsRecord {
390 id: "2".to_string(),
391 last_modified: 200,
392 deleted: false,
393 attachment: None,
394 fields: serde_json::json!({"key": "value2"})
395 .as_object()
396 .unwrap()
397 .clone(),
398 },
399 ];
400
401 storage.insert_collection_content(
403 collection_url,
404 &records,
405 300,
406 CollectionMetadata::default(),
407 )?;
408
409 let fetched_records = storage.get_records(collection_url)?;
411 assert!(fetched_records.is_some());
412 let fetched_records = fetched_records.unwrap();
413 assert_eq!(fetched_records.len(), 2);
414 assert_eq!(fetched_records, records);
415
416 assert_eq!(fetched_records[0].fields["key"], "value1");
417
418 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
420 assert_eq!(last_modified, Some(300));
421
422 Ok(())
423 }
424
425 #[test]
426 fn test_storage_get_records_none() -> Result<()> {
427 let mut storage = Storage::new(":memory:".into());
428
429 let collection_url = "https://example.com/api";
430
431 let fetched_records = storage.get_records(collection_url)?;
433 assert!(fetched_records.is_none());
434
435 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
437 assert!(last_modified.is_none());
438
439 Ok(())
440 }
441
442 #[test]
443 fn test_storage_get_records_empty() -> Result<()> {
444 let mut storage = Storage::new(":memory:".into());
445
446 let collection_url = "https://example.com/api";
447
448 storage.insert_collection_content(
450 collection_url,
451 &Vec::<RemoteSettingsRecord>::default(),
452 42,
453 CollectionMetadata::default(),
454 )?;
455
456 let fetched_records = storage.get_records(collection_url)?;
458 assert_eq!(fetched_records, Some(Vec::new()));
459
460 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
462 assert_eq!(last_modified, Some(42));
463
464 Ok(())
465 }
466
467 #[test]
468 fn test_storage_set_and_get_attachment() -> Result<()> {
469 let mut storage = Storage::new(":memory:".into());
470
471 let attachment = &[0x18, 0x64];
472 let collection_url = "https://example.com/api";
473 let attachment_metadata = Attachment {
474 filename: "abc".to_string(),
475 mimetype: "application/json".to_string(),
476 location: "tmp".to_string(),
477 hash: format!("{:x}", Sha256::digest(attachment)),
478 size: attachment.len() as u64,
479 };
480
481 storage.set_attachment(collection_url, &attachment_metadata.location, attachment)?;
483
484 let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata)?;
486 assert!(fetched_attachment.is_some());
487 let fetched_attachment = fetched_attachment.unwrap();
488 assert_eq!(fetched_attachment, attachment);
489
490 Ok(())
491 }
492
493 #[test]
494 fn test_storage_set_and_replace_attachment() -> Result<()> {
495 let mut storage = Storage::new(":memory:".into());
496
497 let collection_url = "https://example.com/api";
498
499 let attachment_1 = &[0x18, 0x64];
500 let attachment_2 = &[0x12, 0x48];
501
502 let attachment_metadata_1 = Attachment {
503 filename: "abc".to_string(),
504 mimetype: "application/json".to_string(),
505 location: "tmp".to_string(),
506 hash: format!("{:x}", Sha256::digest(attachment_1)),
507 size: attachment_1.len() as u64,
508 };
509
510 let attachment_metadata_2 = Attachment {
511 filename: "def".to_string(),
512 mimetype: "application/json".to_string(),
513 location: "tmp".to_string(),
514 hash: format!("{:x}", Sha256::digest(attachment_2)),
515 size: attachment_2.len() as u64,
516 };
517
518 storage.set_attachment(
520 collection_url,
521 &attachment_metadata_1.location,
522 attachment_1,
523 )?;
524
525 storage.set_attachment(
527 collection_url,
528 &attachment_metadata_2.location,
529 attachment_2,
530 )?;
531
532 let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata_2)?;
534 assert!(fetched_attachment.is_some());
535 let fetched_attachment = fetched_attachment.unwrap();
536 assert_eq!(fetched_attachment, attachment_2);
537
538 Ok(())
539 }
540
541 #[test]
542 fn test_storage_set_attachment_delete_others() -> Result<()> {
543 let mut storage = Storage::new(":memory:".into());
544
545 let collection_url_1 = "https://example.com/api1";
546 let collection_url_2 = "https://example.com/api2";
547
548 let attachment_1 = &[0x18, 0x64];
549 let attachment_2 = &[0x12, 0x48];
550
551 let attachment_metadata_1 = Attachment {
552 filename: "abc".to_string(),
553 mimetype: "application/json".to_string(),
554 location: "first_tmp".to_string(),
555 hash: format!("{:x}", Sha256::digest(attachment_1)),
556 size: attachment_1.len() as u64,
557 };
558
559 let attachment_metadata_2 = Attachment {
560 filename: "def".to_string(),
561 mimetype: "application/json".to_string(),
562 location: "second_tmp".to_string(),
563 hash: format!("{:x}", Sha256::digest(attachment_2)),
564 size: attachment_2.len() as u64,
565 };
566
567 storage.set_attachment(
569 collection_url_1,
570 &attachment_metadata_1.location,
571 attachment_1,
572 )?;
573 storage.set_attachment(
574 collection_url_2,
575 &attachment_metadata_2.location,
576 attachment_2,
577 )?;
578
579 let fetched_attachment_1 =
581 storage.get_attachment(collection_url_1, attachment_metadata_1)?;
582 assert!(fetched_attachment_1.is_none());
583
584 let fetched_attachment_2 =
585 storage.get_attachment(collection_url_2, attachment_metadata_2)?;
586 assert!(fetched_attachment_2.is_some());
587 let fetched_attachment_2 = fetched_attachment_2.unwrap();
588 assert_eq!(fetched_attachment_2, attachment_2);
589
590 Ok(())
591 }
592
593 #[test]
607 fn test_storage_orphaned_attachments_cleaned_up_on_update() -> Result<()> {
608 let mut storage = Storage::new(":memory:".into());
609 let collection_url = "https://example.com/api";
610
611 let attachment_v1 = b"version 1 data";
612 let attachment_v2 = b"version 2 data";
613
614 let attachment_meta_v1 = Attachment {
615 filename: "sponsored-suggestions-us-phone.json".to_string(),
616 mimetype: "application/json".to_string(),
617 location: "main-workspace/quicksuggest-amp/attachment-v1.json".to_string(),
618 hash: format!("{:x}", Sha256::digest(attachment_v1)),
619 size: attachment_v1.len() as u64,
620 };
621
622 let attachment_meta_v2 = Attachment {
623 filename: "sponsored-suggestions-us-phone.json".to_string(),
624 mimetype: "application/json".to_string(),
625 location: "main-workspace/quicksuggest-amp/attachment-v2.json".to_string(),
626 hash: format!("{:x}", Sha256::digest(attachment_v2)),
627 size: attachment_v2.len() as u64,
628 };
629
630 let records_v1 = vec![RemoteSettingsRecord {
632 id: "sponsored-suggestions-us-phone".to_string(),
633 last_modified: 100,
634 deleted: false,
635 attachment: Some(attachment_meta_v1.clone()),
636 fields: serde_json::json!({"type": "amp"})
637 .as_object()
638 .unwrap()
639 .clone(),
640 }];
641
642 storage.insert_collection_content(
643 collection_url,
644 &records_v1,
645 100,
646 CollectionMetadata::default(),
647 )?;
648 storage.set_attachment(collection_url, &attachment_meta_v1.location, attachment_v1)?;
649
650 let fetched = storage.get_attachment(collection_url, attachment_meta_v1.clone())?;
651 assert!(fetched.is_some(), "v1 attachment should be stored");
652
653 let records_v2 = vec![RemoteSettingsRecord {
656 id: "sponsored-suggestions-us-phone".to_string(),
657 last_modified: 200,
658 deleted: false,
659 attachment: Some(attachment_meta_v2.clone()),
660 fields: serde_json::json!({"type": "amp"})
661 .as_object()
662 .unwrap()
663 .clone(),
664 }];
665
666 storage.insert_collection_content(
667 collection_url,
668 &records_v2,
669 200,
670 CollectionMetadata::default(),
671 )?;
672 storage.set_attachment(collection_url, &attachment_meta_v2.location, attachment_v2)?;
673
674 let fetched_v2 = storage.get_attachment(collection_url, attachment_meta_v2)?;
675 assert!(fetched_v2.is_some(), "v2 attachment should be stored");
676
677 let fetched_v1 = storage.get_attachment(collection_url, attachment_meta_v1)?;
678 assert!(
679 fetched_v1.is_none(),
680 "v1 attachment should be cleaned up after record points to v2"
681 );
682
683 Ok(())
684 }
685
686 #[test]
697 fn test_storage_orphaned_attachments_cleaned_up_on_delete() -> Result<()> {
698 let mut storage = Storage::new(":memory:".into());
699 let collection_url = "https://example.com/api";
700
701 let attachment_data = b"sponsored suggestions for GB phone";
702
703 let attachment_meta = Attachment {
704 filename: "sponsored-suggestions-gb-phone.json".to_string(),
705 mimetype: "application/json".to_string(),
706 location: "main-workspace/quicksuggest-amp/attachment-gb.json".to_string(),
707 hash: format!("{:x}", Sha256::digest(attachment_data)),
708 size: attachment_data.len() as u64,
709 };
710
711 let initial_records = vec![
713 RemoteSettingsRecord {
714 id: "sponsored-suggestions-gb-phone".to_string(),
715 last_modified: 100,
716 deleted: false,
717 attachment: Some(attachment_meta.clone()),
718 fields: serde_json::json!({"type": "amp"})
719 .as_object()
720 .unwrap()
721 .clone(),
722 },
723 RemoteSettingsRecord {
724 id: "sponsored-suggestions-us-phone".to_string(),
725 last_modified: 100,
726 deleted: false,
727 attachment: None,
728 fields: serde_json::json!({"type": "amp"})
729 .as_object()
730 .unwrap()
731 .clone(),
732 },
733 ];
734
735 storage.insert_collection_content(
736 collection_url,
737 &initial_records,
738 100,
739 CollectionMetadata::default(),
740 )?;
741 storage.set_attachment(collection_url, &attachment_meta.location, attachment_data)?;
742
743 let fetched = storage.get_attachment(collection_url, attachment_meta.clone())?;
744 assert!(fetched.is_some(), "GB attachment should be stored");
745
746 let updated_records = vec![RemoteSettingsRecord {
748 id: "sponsored-suggestions-gb-phone".to_string(),
749 last_modified: 200,
750 deleted: true,
751 attachment: None,
752 fields: RsJsonObject::new(),
753 }];
754
755 storage.insert_collection_content(
756 collection_url,
757 &updated_records,
758 200,
759 CollectionMetadata::default(),
760 )?;
761
762 let fetched = storage.get_attachment(collection_url, attachment_meta)?;
763 assert!(
764 fetched.is_none(),
765 "GB attachment should be cleaned up after record is deleted via tombstone"
766 );
767
768 Ok(())
769 }
770
771 #[test]
772 fn test_storage_get_attachment_not_found() -> Result<()> {
773 let mut storage = Storage::new(":memory:".into());
774
775 let collection_url = "https://example.com/api";
776 let metadata = Attachment::default();
777
778 let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
780 assert!(fetched_attachment.is_none());
781
782 Ok(())
783 }
784
785 #[test]
786 fn test_storage_empty() -> Result<()> {
787 let mut storage = Storage::new(":memory:".into());
788
789 let collection_url = "https://example.com/api";
790 let attachment = &[0x18, 0x64];
791
792 let records = vec![
793 RemoteSettingsRecord {
794 id: "1".to_string(),
795 last_modified: 100,
796 deleted: false,
797 attachment: None,
798 fields: serde_json::json!({"key": "value1"})
799 .as_object()
800 .unwrap()
801 .clone(),
802 },
803 RemoteSettingsRecord {
804 id: "2".to_string(),
805 last_modified: 200,
806 deleted: false,
807 attachment: Some(Attachment {
808 filename: "abc".to_string(),
809 mimetype: "application/json".to_string(),
810 location: "tmp".to_string(),
811 hash: format!("{:x}", Sha256::digest(attachment)),
812 size: attachment.len() as u64,
813 }),
814 fields: serde_json::json!({"key": "value2"})
815 .as_object()
816 .unwrap()
817 .clone(),
818 },
819 ];
820
821 let metadata = records[1]
822 .clone()
823 .attachment
824 .expect("No attachment metadata for record");
825
826 storage.insert_collection_content(
828 collection_url,
829 &records,
830 42,
831 CollectionMetadata::default(),
832 )?;
833 storage.set_attachment(collection_url, &metadata.location, attachment)?;
834
835 let fetched_records = storage.get_records(collection_url)?;
837 assert!(fetched_records.is_some());
838 let fetched_attachment = storage.get_attachment(collection_url, metadata.clone())?;
839 assert!(fetched_attachment.is_some());
840
841 storage.empty()?;
843
844 let fetched_records = storage.get_records(collection_url)?;
846 assert!(fetched_records.is_none());
847 let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
848 assert!(fetched_attachment.is_none());
849
850 Ok(())
851 }
852
853 #[test]
854 fn test_storage_collection_url_isolation() -> Result<()> {
855 let mut storage = Storage::new(":memory:".into());
856
857 let collection_url1 = "https://example.com/api1";
858 let collection_url2 = "https://example.com/api2";
859 let records_collection_url1 = vec![RemoteSettingsRecord {
860 id: "1".to_string(),
861 last_modified: 100,
862 deleted: false,
863 attachment: None,
864 fields: serde_json::json!({"key": "value1"})
865 .as_object()
866 .unwrap()
867 .clone(),
868 }];
869 let records_collection_url2 = vec![RemoteSettingsRecord {
870 id: "2".to_string(),
871 last_modified: 200,
872 deleted: false,
873 attachment: None,
874 fields: serde_json::json!({"key": "value2"})
875 .as_object()
876 .unwrap()
877 .clone(),
878 }];
879
880 storage.insert_collection_content(
882 collection_url1,
883 &records_collection_url1,
884 42,
885 CollectionMetadata::default(),
886 )?;
887 let fetched_records = storage.get_records(collection_url1)?;
889 assert!(fetched_records.is_some());
890 let fetched_records = fetched_records.unwrap();
891 assert_eq!(fetched_records.len(), 1);
892 assert_eq!(fetched_records, records_collection_url1);
893
894 storage.insert_collection_content(
896 collection_url2,
897 &records_collection_url2,
898 300,
899 CollectionMetadata::default(),
900 )?;
901
902 let fetched_records = storage.get_records(collection_url1)?;
904 assert!(fetched_records.is_none());
905
906 let fetched_records = storage.get_records(collection_url2)?;
908 assert!(fetched_records.is_some());
909 let fetched_records = fetched_records.unwrap();
910 assert_eq!(fetched_records.len(), 1);
911 assert_eq!(fetched_records, records_collection_url2);
912
913 let last_modified1 = storage.get_last_modified_timestamp(collection_url1)?;
915 assert_eq!(last_modified1, None);
916 let last_modified2 = storage.get_last_modified_timestamp(collection_url2)?;
917 assert_eq!(last_modified2, Some(300));
918
919 Ok(())
920 }
921
922 #[test]
923 fn test_storage_insert_collection_content() -> Result<()> {
924 let mut storage = Storage::new(":memory:".into());
925
926 let collection_url = "https://example.com/api";
927 let initial_records = vec![RemoteSettingsRecord {
928 id: "2".to_string(),
929 last_modified: 200,
930 deleted: false,
931 attachment: None,
932 fields: serde_json::json!({"key": "value2"})
933 .as_object()
934 .unwrap()
935 .clone(),
936 }];
937
938 storage.insert_collection_content(
940 collection_url,
941 &initial_records,
942 42,
943 CollectionMetadata::default(),
944 )?;
945
946 let fetched_records = storage.get_records(collection_url)?;
948 assert!(fetched_records.is_some());
949 assert_eq!(fetched_records.unwrap(), initial_records);
950
951 let updated_records = vec![RemoteSettingsRecord {
953 id: "2".to_string(),
954 last_modified: 200,
955 deleted: false,
956 attachment: None,
957 fields: serde_json::json!({"key": "value2_updated"})
958 .as_object()
959 .unwrap()
960 .clone(),
961 }];
962 storage.insert_collection_content(
963 collection_url,
964 &updated_records,
965 300,
966 CollectionMetadata::default(),
967 )?;
968
969 let fetched_records = storage.get_records(collection_url)?;
971 assert!(fetched_records.is_some());
972 assert_eq!(fetched_records.unwrap(), updated_records);
973
974 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
976 assert_eq!(last_modified, Some(300));
977
978 Ok(())
979 }
980
981 fn test_fields(data: &str) -> RsJsonObject {
983 let mut map = serde_json::Map::new();
984 map.insert("data".into(), data.into());
985 map
986 }
987
988 #[test]
989 fn test_storage_merge_records() -> Result<()> {
990 let mut storage = Storage::new(":memory:".into());
991
992 let collection_url = "https://example.com/api";
993
994 let initial_records = vec![
995 RemoteSettingsRecord {
996 id: "a".into(),
997 last_modified: 100,
998 deleted: false,
999 attachment: None,
1000 fields: test_fields("a"),
1001 },
1002 RemoteSettingsRecord {
1003 id: "b".into(),
1004 last_modified: 200,
1005 deleted: false,
1006 attachment: None,
1007 fields: test_fields("b"),
1008 },
1009 RemoteSettingsRecord {
1010 id: "c".into(),
1011 last_modified: 300,
1012 deleted: false,
1013 attachment: None,
1014 fields: test_fields("c"),
1015 },
1016 ];
1017 let updated_records = vec![
1018 RemoteSettingsRecord {
1020 id: "d".into(),
1021 last_modified: 1300,
1022 deleted: false,
1023 attachment: None,
1024 fields: test_fields("d"),
1025 },
1026 RemoteSettingsRecord {
1028 id: "b".into(),
1029 last_modified: 1200,
1030 deleted: true,
1031 attachment: None,
1032 fields: RsJsonObject::new(),
1033 },
1034 RemoteSettingsRecord {
1036 id: "a".into(),
1037 last_modified: 1100,
1038 deleted: false,
1039 attachment: None,
1040 fields: test_fields("a-with-new-data"),
1041 },
1042 ];
1044 let expected_records = vec![
1045 RemoteSettingsRecord {
1047 id: "a".into(),
1048 last_modified: 1100,
1049 deleted: false,
1050 attachment: None,
1051 fields: test_fields("a-with-new-data"),
1052 },
1053 RemoteSettingsRecord {
1054 id: "c".into(),
1055 last_modified: 300,
1056 deleted: false,
1057 attachment: None,
1058 fields: test_fields("c"),
1059 },
1060 RemoteSettingsRecord {
1061 id: "d".into(),
1062 last_modified: 1300,
1063 deleted: false,
1064 attachment: None,
1065 fields: test_fields("d"),
1066 },
1067 ];
1068
1069 storage.insert_collection_content(
1071 collection_url,
1072 &initial_records,
1073 1000,
1074 CollectionMetadata::default(),
1075 )?;
1076
1077 let fetched_records = storage.get_records(collection_url)?.unwrap();
1079 assert_eq!(fetched_records, initial_records);
1080
1081 storage.insert_collection_content(
1083 collection_url,
1084 &updated_records,
1085 1300,
1086 CollectionMetadata::default(),
1087 )?;
1088
1089 let mut fetched_records = storage.get_records(collection_url)?.unwrap();
1091 fetched_records.sort_by_cached_key(|r| r.id.clone());
1092 assert_eq!(fetched_records, expected_records);
1093
1094 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
1096 assert_eq!(last_modified, Some(1300));
1097 Ok(())
1098 }
1099 #[test]
1100 fn test_storage_get_collection_metadata() -> Result<()> {
1101 let mut storage = Storage::new(":memory:".into());
1102
1103 let collection_url = "https://example.com/api";
1104 let initial_records = vec![RemoteSettingsRecord {
1105 id: "2".to_string(),
1106 last_modified: 200,
1107 deleted: false,
1108 attachment: None,
1109 fields: serde_json::json!({"key": "value2"})
1110 .as_object()
1111 .unwrap()
1112 .clone(),
1113 }];
1114
1115 storage.insert_collection_content(
1117 collection_url,
1118 &initial_records,
1119 1337,
1120 CollectionMetadata {
1121 bucket: "main".into(),
1122 signatures: vec![
1123 CollectionSignature {
1124 signature: "b64encodedsig".into(),
1125 x5u: "http://15u/".into(),
1126 },
1127 CollectionSignature {
1128 signature: "b64encodedsig2".into(),
1129 x5u: "http://15u2/".into(),
1130 },
1131 ],
1132 },
1133 )?;
1134
1135 let metadata = storage.get_collection_metadata(collection_url)?.unwrap();
1136
1137 assert_eq!(metadata.signatures[0].signature, "b64encodedsig");
1138 assert_eq!(metadata.signatures[0].x5u, "http://15u/");
1139 assert_eq!(metadata.signatures[1].signature, "b64encodedsig2");
1140 assert_eq!(metadata.signatures[1].x5u, "http://15u2/");
1141
1142 Ok(())
1143 }
1144}