remote_settings/
storage.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use 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
17/// Internal storage type
18///
19/// This will store downloaded records/attachments in a SQLite database.  Nothing is implemented
20/// yet other than the initial API.
21///
22/// Most methods input a `collection_url` parameter, is a URL that includes the remote settings
23/// server, bucket, and collection. If the `collection_url` for a get method does not match the one
24/// for a set method, then this means the application has switched their remote settings config and
25/// [Storage] should pretend like nothing is stored in the database.
26///
27/// The reason for this is the [crate::RemoteSettingsService::update_config] method.  If a consumer
28/// passes a new server or bucket to `update_config`, we don't want to be using cached data from
29/// the previous config.
30pub 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                // Another thread created the directory, just ignore the error.
73                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    /// Get the last modified timestamp for the stored records
85    ///
86    /// Returns None if no records are stored or if `collection_url` does not match the
87    /// last `collection_url` passed to `insert_collection_content`
88    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    /// Get cached records for this collection
99    ///
100    /// Returns None if no records are stored or if `collection_url` does not match the `collection_url` passed
101    /// to `insert_collection_content`.
102    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            // If fetched before, get the records from the records table
114            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    /// Get cached metadata for this collection
130    ///
131    /// Returns None if no data is stored or if `collection_url` does not match the `collection_url` passed
132    /// to `insert_collection_content`.
133    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(
139            "SELECT bucket, signature, x5u FROM collection_metadata WHERE collection_url = ?",
140        )?;
141
142        if let Some(metadata) = stmt_metadata
143            .query_row(params![collection_url], |row| {
144                Ok(CollectionMetadata {
145                    bucket: row.get(0).unwrap_or_default(),
146                    signature: CollectionSignature {
147                        signature: row.get(1).unwrap_or_default(),
148                        x5u: row.get(2).unwrap_or_default(),
149                    },
150                })
151            })
152            .optional()?
153        {
154            Ok(Some(metadata))
155        } else {
156            Ok(None)
157        }
158    }
159
160    /// Get cached attachment data
161    ///
162    /// This returns the last attachment data sent to [Self::set_attachment].
163    ///
164    /// Returns None if no attachment data is stored or if `collection_url` does not match the `collection_url`
165    /// passed to `set_attachment`.
166    pub fn get_attachment(
167        &mut self,
168        collection_url: &str,
169        metadata: Attachment,
170    ) -> Result<Option<Vec<u8>>> {
171        let tx = self.transaction()?;
172        let mut stmt =
173            tx.prepare("SELECT data FROM attachments WHERE id = ? AND collection_url = ?")?;
174
175        if let Some(data) = stmt
176            .query_row((metadata.location, collection_url), |row| {
177                row.get::<_, Vec<u8>>(0)
178            })
179            .optional()?
180        {
181            // Return None if data doesn't match expected metadata
182            if data.len() as u64 != metadata.size {
183                return Ok(None);
184            }
185            let hash = format!("{:x}", Sha256::digest(&data));
186            if hash != metadata.hash {
187                return Ok(None);
188            }
189            Ok(Some(data))
190        } else {
191            Ok(None)
192        }
193    }
194
195    /// Set cached content for this collection.
196    pub fn insert_collection_content(
197        &mut self,
198        collection_url: &str,
199        records: &[RemoteSettingsRecord],
200        last_modified: u64,
201        metadata: CollectionMetadata,
202    ) -> Result<()> {
203        let tx = self.transaction()?;
204
205        // Delete ALL existing records and metadata for with different collection_urls.
206        //
207        // This way, if a user (probably QA) switches the remote settings server in the middle of a
208        // browser sessions, we'll delete the stale data from the previous server.
209        tx.execute(
210            "DELETE FROM records where collection_url <> ?",
211            [collection_url],
212        )?;
213        tx.execute(
214            "DELETE FROM collection_metadata where collection_url <> ?",
215            [collection_url],
216        )?;
217
218        Self::update_record_rows(&tx, collection_url, records)?;
219        Self::update_collection_metadata(&tx, collection_url, last_modified, metadata)?;
220        tx.commit()?;
221        Ok(())
222    }
223
224    /// Insert/remove/update rows in the records table based on a records list
225    ///
226    /// Returns the max last modified record from the list
227    fn update_record_rows(
228        tx: &Transaction<'_>,
229        collection_url: &str,
230        records: &[RemoteSettingsRecord],
231    ) -> Result<u64> {
232        // Find the max last_modified time while inserting records
233        let mut max_last_modified = 0;
234        {
235            let mut insert_stmt = tx.prepare(
236                "INSERT OR REPLACE INTO records (id, collection_url, data) VALUES (?, ?, ?)",
237            )?;
238            let mut delete_stmt = tx.prepare("DELETE FROM records WHERE id=?")?;
239            for record in records {
240                if record.deleted {
241                    delete_stmt.execute(params![&record.id])?;
242                } else {
243                    max_last_modified = max_last_modified.max(record.last_modified);
244                    let data = serde_json::to_vec(&record)?;
245                    insert_stmt.execute(params![record.id, collection_url, data])?;
246                }
247            }
248        }
249        Ok(max_last_modified)
250    }
251
252    /// Update the collection metadata after setting/merging records
253    fn update_collection_metadata(
254        tx: &Transaction<'_>,
255        collection_url: &str,
256        last_modified: u64,
257        metadata: CollectionMetadata,
258    ) -> Result<()> {
259        // Update the metadata
260        tx.execute(
261            "INSERT OR REPLACE INTO collection_metadata \
262            (collection_url, last_modified, bucket, signature, x5u) \
263            VALUES (?, ?, ?, ?, ?)",
264            (
265                collection_url,
266                last_modified,
267                metadata.bucket,
268                metadata.signature.signature,
269                metadata.signature.x5u,
270            ),
271        )?;
272        Ok(())
273    }
274
275    /// Set the attachment data stored in the database, clearing out any previously stored data
276    pub fn set_attachment(
277        &mut self,
278        collection_url: &str,
279        location: &str,
280        attachment: &[u8],
281    ) -> Result<()> {
282        let tx = self.transaction()?;
283
284        // Delete ALL existing attachments for every collection_url
285        tx.execute(
286            "DELETE FROM attachments WHERE collection_url != ?",
287            params![collection_url],
288        )?;
289
290        tx.execute(
291            "INSERT OR REPLACE INTO ATTACHMENTS \
292            (id, collection_url, data) \
293            VALUES (?, ?, ?)",
294            params![location, collection_url, attachment,],
295        )?;
296
297        tx.commit()?;
298
299        Ok(())
300    }
301
302    /// Empty out all cached values and start from scratch.  This is called when
303    /// RemoteSettingsService::update_config() is called, since that could change the remote
304    /// settings server which would invalidate all cached data.
305    pub fn empty(&mut self) -> Result<()> {
306        let tx = self.transaction()?;
307        tx.execute("DELETE FROM records", [])?;
308        tx.execute("DELETE FROM attachments", [])?;
309        tx.execute("DELETE FROM collection_metadata", [])?;
310        tx.commit()?;
311        Ok(())
312    }
313}
314
315/// Stores the SQLite connection, which is lazily constructed and can be closed/shutdown.
316enum ConnectionCell {
317    Uninitialized,
318    Initialized(Connection),
319    Closed,
320}
321
322#[cfg(test)]
323mod tests {
324    use super::Storage;
325    use crate::{
326        client::CollectionMetadata, client::CollectionSignature, Attachment, RemoteSettingsRecord,
327        Result, RsJsonObject,
328    };
329    use sha2::{Digest, Sha256};
330
331    #[test]
332    fn test_storage_set_and_get_records() -> Result<()> {
333        let mut storage = Storage::new(":memory:".into());
334
335        let collection_url = "https://example.com/api";
336        let records = vec![
337            RemoteSettingsRecord {
338                id: "1".to_string(),
339                last_modified: 100,
340                deleted: false,
341                attachment: None,
342                fields: serde_json::json!({"key": "value1"})
343                    .as_object()
344                    .unwrap()
345                    .clone(),
346            },
347            RemoteSettingsRecord {
348                id: "2".to_string(),
349                last_modified: 200,
350                deleted: false,
351                attachment: None,
352                fields: serde_json::json!({"key": "value2"})
353                    .as_object()
354                    .unwrap()
355                    .clone(),
356            },
357        ];
358
359        // Set records
360        storage.insert_collection_content(
361            collection_url,
362            &records,
363            300,
364            CollectionMetadata::default(),
365        )?;
366
367        // Get records
368        let fetched_records = storage.get_records(collection_url)?;
369        assert!(fetched_records.is_some());
370        let fetched_records = fetched_records.unwrap();
371        assert_eq!(fetched_records.len(), 2);
372        assert_eq!(fetched_records, records);
373
374        assert_eq!(fetched_records[0].fields["key"], "value1");
375
376        // Get last modified timestamp
377        let last_modified = storage.get_last_modified_timestamp(collection_url)?;
378        assert_eq!(last_modified, Some(300));
379
380        Ok(())
381    }
382
383    #[test]
384    fn test_storage_get_records_none() -> Result<()> {
385        let mut storage = Storage::new(":memory:".into());
386
387        let collection_url = "https://example.com/api";
388
389        // Get records when none are set
390        let fetched_records = storage.get_records(collection_url)?;
391        assert!(fetched_records.is_none());
392
393        // Get last modified timestamp when no records
394        let last_modified = storage.get_last_modified_timestamp(collection_url)?;
395        assert!(last_modified.is_none());
396
397        Ok(())
398    }
399
400    #[test]
401    fn test_storage_get_records_empty() -> Result<()> {
402        let mut storage = Storage::new(":memory:".into());
403
404        let collection_url = "https://example.com/api";
405
406        // Set empty records
407        storage.insert_collection_content(
408            collection_url,
409            &Vec::<RemoteSettingsRecord>::default(),
410            42,
411            CollectionMetadata::default(),
412        )?;
413
414        // Get records
415        let fetched_records = storage.get_records(collection_url)?;
416        assert_eq!(fetched_records, Some(Vec::new()));
417
418        // Get last modified timestamp when no records
419        let last_modified = storage.get_last_modified_timestamp(collection_url)?;
420        assert_eq!(last_modified, Some(42));
421
422        Ok(())
423    }
424
425    #[test]
426    fn test_storage_set_and_get_attachment() -> Result<()> {
427        let mut storage = Storage::new(":memory:".into());
428
429        let attachment = &[0x18, 0x64];
430        let collection_url = "https://example.com/api";
431        let attachment_metadata = Attachment {
432            filename: "abc".to_string(),
433            mimetype: "application/json".to_string(),
434            location: "tmp".to_string(),
435            hash: format!("{:x}", Sha256::digest(attachment)),
436            size: attachment.len() as u64,
437        };
438
439        // Store attachment
440        storage.set_attachment(collection_url, &attachment_metadata.location, attachment)?;
441
442        // Get attachment
443        let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata)?;
444        assert!(fetched_attachment.is_some());
445        let fetched_attachment = fetched_attachment.unwrap();
446        assert_eq!(fetched_attachment, attachment);
447
448        Ok(())
449    }
450
451    #[test]
452    fn test_storage_set_and_replace_attachment() -> Result<()> {
453        let mut storage = Storage::new(":memory:".into());
454
455        let collection_url = "https://example.com/api";
456
457        let attachment_1 = &[0x18, 0x64];
458        let attachment_2 = &[0x12, 0x48];
459
460        let attachment_metadata_1 = Attachment {
461            filename: "abc".to_string(),
462            mimetype: "application/json".to_string(),
463            location: "tmp".to_string(),
464            hash: format!("{:x}", Sha256::digest(attachment_1)),
465            size: attachment_1.len() as u64,
466        };
467
468        let attachment_metadata_2 = Attachment {
469            filename: "def".to_string(),
470            mimetype: "application/json".to_string(),
471            location: "tmp".to_string(),
472            hash: format!("{:x}", Sha256::digest(attachment_2)),
473            size: attachment_2.len() as u64,
474        };
475
476        // Store first attachment
477        storage.set_attachment(
478            collection_url,
479            &attachment_metadata_1.location,
480            attachment_1,
481        )?;
482
483        // Replace attachment with new data
484        storage.set_attachment(
485            collection_url,
486            &attachment_metadata_2.location,
487            attachment_2,
488        )?;
489
490        // Get attachment
491        let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata_2)?;
492        assert!(fetched_attachment.is_some());
493        let fetched_attachment = fetched_attachment.unwrap();
494        assert_eq!(fetched_attachment, attachment_2);
495
496        Ok(())
497    }
498
499    #[test]
500    fn test_storage_set_attachment_delete_others() -> Result<()> {
501        let mut storage = Storage::new(":memory:".into());
502
503        let collection_url_1 = "https://example.com/api1";
504        let collection_url_2 = "https://example.com/api2";
505
506        let attachment_1 = &[0x18, 0x64];
507        let attachment_2 = &[0x12, 0x48];
508
509        let attachment_metadata_1 = Attachment {
510            filename: "abc".to_string(),
511            mimetype: "application/json".to_string(),
512            location: "first_tmp".to_string(),
513            hash: format!("{:x}", Sha256::digest(attachment_1)),
514            size: attachment_1.len() as u64,
515        };
516
517        let attachment_metadata_2 = Attachment {
518            filename: "def".to_string(),
519            mimetype: "application/json".to_string(),
520            location: "second_tmp".to_string(),
521            hash: format!("{:x}", Sha256::digest(attachment_2)),
522            size: attachment_2.len() as u64,
523        };
524
525        // Set attachments for two different collections
526        storage.set_attachment(
527            collection_url_1,
528            &attachment_metadata_1.location,
529            attachment_1,
530        )?;
531        storage.set_attachment(
532            collection_url_2,
533            &attachment_metadata_2.location,
534            attachment_2,
535        )?;
536
537        // Verify that only the attachment for the second collection remains
538        let fetched_attachment_1 =
539            storage.get_attachment(collection_url_1, attachment_metadata_1)?;
540        assert!(fetched_attachment_1.is_none());
541
542        let fetched_attachment_2 =
543            storage.get_attachment(collection_url_2, attachment_metadata_2)?;
544        assert!(fetched_attachment_2.is_some());
545        let fetched_attachment_2 = fetched_attachment_2.unwrap();
546        assert_eq!(fetched_attachment_2, attachment_2);
547
548        Ok(())
549    }
550
551    #[test]
552    fn test_storage_get_attachment_not_found() -> Result<()> {
553        let mut storage = Storage::new(":memory:".into());
554
555        let collection_url = "https://example.com/api";
556        let metadata = Attachment::default();
557
558        // Get attachment that doesn't exist
559        let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
560        assert!(fetched_attachment.is_none());
561
562        Ok(())
563    }
564
565    #[test]
566    fn test_storage_empty() -> Result<()> {
567        let mut storage = Storage::new(":memory:".into());
568
569        let collection_url = "https://example.com/api";
570        let attachment = &[0x18, 0x64];
571
572        let records = vec![
573            RemoteSettingsRecord {
574                id: "1".to_string(),
575                last_modified: 100,
576                deleted: false,
577                attachment: None,
578                fields: serde_json::json!({"key": "value1"})
579                    .as_object()
580                    .unwrap()
581                    .clone(),
582            },
583            RemoteSettingsRecord {
584                id: "2".to_string(),
585                last_modified: 200,
586                deleted: false,
587                attachment: Some(Attachment {
588                    filename: "abc".to_string(),
589                    mimetype: "application/json".to_string(),
590                    location: "tmp".to_string(),
591                    hash: format!("{:x}", Sha256::digest(attachment)),
592                    size: attachment.len() as u64,
593                }),
594                fields: serde_json::json!({"key": "value2"})
595                    .as_object()
596                    .unwrap()
597                    .clone(),
598            },
599        ];
600
601        let metadata = records[1]
602            .clone()
603            .attachment
604            .expect("No attachment metadata for record");
605
606        // Set records and attachment
607        storage.insert_collection_content(
608            collection_url,
609            &records,
610            42,
611            CollectionMetadata::default(),
612        )?;
613        storage.set_attachment(collection_url, &metadata.location, attachment)?;
614
615        // Verify they are stored
616        let fetched_records = storage.get_records(collection_url)?;
617        assert!(fetched_records.is_some());
618        let fetched_attachment = storage.get_attachment(collection_url, metadata.clone())?;
619        assert!(fetched_attachment.is_some());
620
621        // Empty the storage
622        storage.empty()?;
623
624        // Verify they are deleted
625        let fetched_records = storage.get_records(collection_url)?;
626        assert!(fetched_records.is_none());
627        let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
628        assert!(fetched_attachment.is_none());
629
630        Ok(())
631    }
632
633    #[test]
634    fn test_storage_collection_url_isolation() -> Result<()> {
635        let mut storage = Storage::new(":memory:".into());
636
637        let collection_url1 = "https://example.com/api1";
638        let collection_url2 = "https://example.com/api2";
639        let records_collection_url1 = vec![RemoteSettingsRecord {
640            id: "1".to_string(),
641            last_modified: 100,
642            deleted: false,
643            attachment: None,
644            fields: serde_json::json!({"key": "value1"})
645                .as_object()
646                .unwrap()
647                .clone(),
648        }];
649        let records_collection_url2 = vec![RemoteSettingsRecord {
650            id: "2".to_string(),
651            last_modified: 200,
652            deleted: false,
653            attachment: None,
654            fields: serde_json::json!({"key": "value2"})
655                .as_object()
656                .unwrap()
657                .clone(),
658        }];
659
660        // Set records for collection_url1
661        storage.insert_collection_content(
662            collection_url1,
663            &records_collection_url1,
664            42,
665            CollectionMetadata::default(),
666        )?;
667        // Verify records for collection_url1
668        let fetched_records = storage.get_records(collection_url1)?;
669        assert!(fetched_records.is_some());
670        let fetched_records = fetched_records.unwrap();
671        assert_eq!(fetched_records.len(), 1);
672        assert_eq!(fetched_records, records_collection_url1);
673
674        // Set records for collection_url2, which will clear records for all collections
675        storage.insert_collection_content(
676            collection_url2,
677            &records_collection_url2,
678            300,
679            CollectionMetadata::default(),
680        )?;
681
682        // Verify that records for collection_url1 have been cleared
683        let fetched_records = storage.get_records(collection_url1)?;
684        assert!(fetched_records.is_none());
685
686        // Verify records for collection_url2 are correctly stored
687        let fetched_records = storage.get_records(collection_url2)?;
688        assert!(fetched_records.is_some());
689        let fetched_records = fetched_records.unwrap();
690        assert_eq!(fetched_records.len(), 1);
691        assert_eq!(fetched_records, records_collection_url2);
692
693        // Verify last modified timestamps only for collection_url2
694        let last_modified1 = storage.get_last_modified_timestamp(collection_url1)?;
695        assert_eq!(last_modified1, None);
696        let last_modified2 = storage.get_last_modified_timestamp(collection_url2)?;
697        assert_eq!(last_modified2, Some(300));
698
699        Ok(())
700    }
701
702    #[test]
703    fn test_storage_insert_collection_content() -> Result<()> {
704        let mut storage = Storage::new(":memory:".into());
705
706        let collection_url = "https://example.com/api";
707        let initial_records = vec![RemoteSettingsRecord {
708            id: "2".to_string(),
709            last_modified: 200,
710            deleted: false,
711            attachment: None,
712            fields: serde_json::json!({"key": "value2"})
713                .as_object()
714                .unwrap()
715                .clone(),
716        }];
717
718        // Set initial records
719        storage.insert_collection_content(
720            collection_url,
721            &initial_records,
722            42,
723            CollectionMetadata::default(),
724        )?;
725
726        // Verify initial records
727        let fetched_records = storage.get_records(collection_url)?;
728        assert!(fetched_records.is_some());
729        assert_eq!(fetched_records.unwrap(), initial_records);
730
731        // Update records
732        let updated_records = vec![RemoteSettingsRecord {
733            id: "2".to_string(),
734            last_modified: 200,
735            deleted: false,
736            attachment: None,
737            fields: serde_json::json!({"key": "value2_updated"})
738                .as_object()
739                .unwrap()
740                .clone(),
741        }];
742        storage.insert_collection_content(
743            collection_url,
744            &updated_records,
745            300,
746            CollectionMetadata::default(),
747        )?;
748
749        // Verify updated records
750        let fetched_records = storage.get_records(collection_url)?;
751        assert!(fetched_records.is_some());
752        assert_eq!(fetched_records.unwrap(), updated_records);
753
754        // Verify last modified timestamp
755        let last_modified = storage.get_last_modified_timestamp(collection_url)?;
756        assert_eq!(last_modified, Some(300));
757
758        Ok(())
759    }
760
761    // Quick way to generate the fields data for our mock records
762    fn test_fields(data: &str) -> RsJsonObject {
763        let mut map = serde_json::Map::new();
764        map.insert("data".into(), data.into());
765        map
766    }
767
768    #[test]
769    fn test_storage_merge_records() -> Result<()> {
770        let mut storage = Storage::new(":memory:".into());
771
772        let collection_url = "https://example.com/api";
773
774        let initial_records = vec![
775            RemoteSettingsRecord {
776                id: "a".into(),
777                last_modified: 100,
778                deleted: false,
779                attachment: None,
780                fields: test_fields("a"),
781            },
782            RemoteSettingsRecord {
783                id: "b".into(),
784                last_modified: 200,
785                deleted: false,
786                attachment: None,
787                fields: test_fields("b"),
788            },
789            RemoteSettingsRecord {
790                id: "c".into(),
791                last_modified: 300,
792                deleted: false,
793                attachment: None,
794                fields: test_fields("c"),
795            },
796        ];
797        let updated_records = vec![
798            // d is new
799            RemoteSettingsRecord {
800                id: "d".into(),
801                last_modified: 1300,
802                deleted: false,
803                attachment: None,
804                fields: test_fields("d"),
805            },
806            // b was deleted
807            RemoteSettingsRecord {
808                id: "b".into(),
809                last_modified: 1200,
810                deleted: true,
811                attachment: None,
812                fields: RsJsonObject::new(),
813            },
814            // a was updated
815            RemoteSettingsRecord {
816                id: "a".into(),
817                last_modified: 1100,
818                deleted: false,
819                attachment: None,
820                fields: test_fields("a-with-new-data"),
821            },
822            // c was not modified, so it's not present in the new response
823        ];
824        let expected_records = vec![
825            // a was updated
826            RemoteSettingsRecord {
827                id: "a".into(),
828                last_modified: 1100,
829                deleted: false,
830                attachment: None,
831                fields: test_fields("a-with-new-data"),
832            },
833            RemoteSettingsRecord {
834                id: "c".into(),
835                last_modified: 300,
836                deleted: false,
837                attachment: None,
838                fields: test_fields("c"),
839            },
840            RemoteSettingsRecord {
841                id: "d".into(),
842                last_modified: 1300,
843                deleted: false,
844                attachment: None,
845                fields: test_fields("d"),
846            },
847        ];
848
849        // Set initial records
850        storage.insert_collection_content(
851            collection_url,
852            &initial_records,
853            1000,
854            CollectionMetadata::default(),
855        )?;
856
857        // Verify initial records
858        let fetched_records = storage.get_records(collection_url)?.unwrap();
859        assert_eq!(fetched_records, initial_records);
860
861        // Update records
862        storage.insert_collection_content(
863            collection_url,
864            &updated_records,
865            1300,
866            CollectionMetadata::default(),
867        )?;
868
869        // Verify updated records
870        let mut fetched_records = storage.get_records(collection_url)?.unwrap();
871        fetched_records.sort_by_cached_key(|r| r.id.clone());
872        assert_eq!(fetched_records, expected_records);
873
874        // Verify last modified timestamp
875        let last_modified = storage.get_last_modified_timestamp(collection_url)?;
876        assert_eq!(last_modified, Some(1300));
877        Ok(())
878    }
879    #[test]
880    fn test_storage_get_collection_metadata() -> Result<()> {
881        let mut storage = Storage::new(":memory:".into());
882
883        let collection_url = "https://example.com/api";
884        let initial_records = vec![RemoteSettingsRecord {
885            id: "2".to_string(),
886            last_modified: 200,
887            deleted: false,
888            attachment: None,
889            fields: serde_json::json!({"key": "value2"})
890                .as_object()
891                .unwrap()
892                .clone(),
893        }];
894
895        // Set initial records
896        storage.insert_collection_content(
897            collection_url,
898            &initial_records,
899            1337,
900            CollectionMetadata {
901                bucket: "main".into(),
902                signature: CollectionSignature {
903                    signature: "b64encodedsig".into(),
904                    x5u: "http://15u/".into(),
905                },
906            },
907        )?;
908
909        let metadata = storage.get_collection_metadata(collection_url)?.unwrap();
910
911        assert_eq!(metadata.signature.signature, "b64encodedsig");
912        assert_eq!(metadata.signature.x5u, "http://15u/");
913
914        Ok(())
915    }
916}