tabs/
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
5// How long we expect a remote command to live. After this time we assume it's
6// either been delivered or will not be.
7// Matches COMMAND_TTL in close_tabs.rs in fxa-client.
8const REMOTE_COMMAND_TTL_MS: u64 = 2 * 24 * 60 * 60 * 1000; // 48 hours.
9
10use crate::error::*;
11use crate::schema;
12use crate::sync::TabsRecord;
13use crate::DeviceType;
14use crate::{PendingCommand, RemoteCommand, Timestamp};
15use error_support::{error, info, trace, warn};
16use rusqlite::{
17    types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef},
18    Connection, OpenFlags,
19};
20use sql_support::open_database::{self, open_database_with_flags};
21use sql_support::ConnExt;
22use std::cell::RefCell;
23use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25use sync15::{RemoteClient, ServerTimestamp};
26pub type TabsDeviceType = crate::DeviceType;
27
28pub(crate) const TABS_CLIENT_TTL: u32 = 15_552_000; // 180 days, same as CLIENTS_TTL
29const FAR_FUTURE: i64 = 4_102_405_200_000; // 2100/01/01
30
31#[derive(Clone, Debug, Default, uniffi::Record)]
32#[cfg_attr(test, derive(PartialEq, Eq))] // only tests need this
33pub struct RemoteTabRecord {
34    pub title: String,
35    pub url_history: Vec<String>,
36    pub icon: Option<String>,
37    pub last_used: i64, // In ms.
38    #[uniffi(default = false)]
39    pub inactive: bool,
40    #[uniffi(default = false)]
41    pub pinned: bool,
42    /// The index within the window_id.
43    #[uniffi(default = 0)]
44    pub index: u32,
45    #[uniffi(default = "")]
46    pub window_id: String,
47    #[uniffi(default = "")]
48    pub tab_group_id: String,
49}
50pub type RemoteTab = RemoteTabRecord;
51
52#[derive(Clone, Debug, uniffi::Record)]
53pub struct ClientRemoteTabs {
54    /// misnamed: this is the fxa_device_id of the client (which may or may not be the same as the corresponding ID in the `clients` collection)
55    pub client_id: String,
56    pub client_name: String,
57    pub device_type: DeviceType,
58    /// Number of ms since the unix epoch (as reported by the server's clock)
59    pub last_modified: i64,
60    pub remote_tabs: Vec<RemoteTab>,
61    pub tab_groups: HashMap<String, TabGroup>,
62    pub windows: HashMap<String, Window>,
63}
64
65#[derive(uniffi::Enum, Clone, Debug, Default)]
66#[repr(u8)]
67pub enum WindowType {
68    #[default]
69    Normal = 0,
70}
71
72impl From<u8> for WindowType {
73    fn from(value: u8) -> Self {
74        match value {
75            0 => WindowType::Normal,
76            _ => {
77                warn!("Unknown window type {}, defaulting to Normal", value);
78                WindowType::Normal
79            }
80        }
81    }
82}
83
84#[derive(uniffi::Record, Debug, Clone)]
85#[cfg_attr(test, derive(Default))]
86pub struct Window {
87    pub id: String,
88    pub last_used: Timestamp,
89    pub index: u32,
90    pub window_type: WindowType,
91}
92
93/// A tab-group, representing a session store `TabGroupStateData`.
94#[derive(uniffi::Record, Debug, Clone)]
95#[cfg_attr(test, derive(Default))]
96pub struct TabGroup {
97    pub id: String,
98    pub name: String,
99    pub color: String,
100    pub collapsed: bool,
101}
102
103// This is what we expect clients to supply as their own tabs.
104#[derive(uniffi::Record, Debug, Clone, Default)]
105pub struct LocalTabsInfo {
106    pub tabs: Vec<RemoteTab>,
107    pub tab_groups: HashMap<String, TabGroup>,
108    pub windows: HashMap<String, Window>,
109}
110
111pub(crate) enum DbConnection {
112    Created,
113    Open(Connection),
114    Closed,
115}
116
117// Tabs has unique requirements for storage:
118// * The "local_tabs" exist only so we can sync them out. There's no facility to
119//   query "local tabs", so there's no need to store these persistently - ie, they
120//   are write-only.
121// * The "remote_tabs" exist purely for incoming items via sync - there's no facility
122//   to set them locally - they are read-only.
123// Note that this means a database is only actually needed after Sync fetches remote tabs,
124// and because sync users are in the minority, the use of a database here is purely
125// optional and created on demand. The implication here is that asking for the "remote tabs"
126// when no database exists is considered a normal situation and just implies no remote tabs exist.
127// (Note however we don't attempt to remove the database when no remote tabs exist, so having
128// no remote tabs in an existing DB is also a normal situation)
129pub struct TabsStorage {
130    pub(crate) local_tabs: RefCell<Option<LocalTabsInfo>>,
131    db_path: PathBuf,
132    db_connection: DbConnection,
133}
134
135impl TabsStorage {
136    pub fn new(db_path: impl AsRef<Path>) -> Self {
137        Self {
138            local_tabs: RefCell::default(),
139            db_path: db_path.as_ref().to_path_buf(),
140            db_connection: DbConnection::Created,
141        }
142    }
143
144    pub fn close(&mut self) {
145        if let DbConnection::Open(conn) =
146            std::mem::replace(&mut self.db_connection, DbConnection::Closed)
147        {
148            if let Err(err) = conn.close() {
149                // Log the error, but continue with shutdown
150                error!("Failed to close the connection: {:?}", err);
151            }
152        }
153    }
154
155    /// Arrange for a new memory-based TabsStorage. As per other DB semantics, creating
156    /// this isn't enough to actually create the db!
157    pub fn new_with_mem_path(db_path: &str) -> Self {
158        let name = PathBuf::from(format!("file:{}?mode=memory&cache=shared", db_path));
159        Self::new(name)
160    }
161
162    /// If a DB file exists, open and return it.
163    pub fn open_if_exists(&mut self) -> Result<Option<&Connection>> {
164        match self.db_connection {
165            DbConnection::Open(ref conn) => return Ok(Some(conn)),
166            DbConnection::Closed => return Ok(None),
167            DbConnection::Created => {}
168        }
169        let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
170            | OpenFlags::SQLITE_OPEN_URI
171            | OpenFlags::SQLITE_OPEN_READ_WRITE;
172        match open_database_with_flags(
173            self.db_path.clone(),
174            flags,
175            &crate::schema::TabsMigrationLogic,
176        ) {
177            Ok(conn) => {
178                info!("tabs storage is opening an existing database");
179                self.db_connection = DbConnection::Open(conn);
180                match self.db_connection {
181                    DbConnection::Open(ref conn) => Ok(Some(conn)),
182                    _ => unreachable!("impossible value"),
183                }
184            }
185            Err(open_database::Error::SqlError(rusqlite::Error::SqliteFailure(code, _)))
186                if code.code == rusqlite::ErrorCode::CannotOpen =>
187            {
188                info!("tabs storage could not open an existing database and hasn't been asked to create one");
189                Ok(None)
190            }
191            Err(e) => Err(e.into()),
192        }
193    }
194
195    /// Open and return the DB, creating it if necessary.
196    pub fn open_or_create(&mut self) -> Result<&Connection> {
197        match self.db_connection {
198            DbConnection::Open(ref conn) => return Ok(conn),
199            DbConnection::Closed => return Err(Error::UnexpectedConnectionState),
200            DbConnection::Created => {}
201        }
202        let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
203            | OpenFlags::SQLITE_OPEN_URI
204            | OpenFlags::SQLITE_OPEN_READ_WRITE
205            | OpenFlags::SQLITE_OPEN_CREATE;
206        let conn = open_database_with_flags(
207            self.db_path.clone(),
208            flags,
209            &crate::schema::TabsMigrationLogic,
210        )?;
211        info!("tabs storage is creating a database connection");
212        self.db_connection = DbConnection::Open(conn);
213        match self.db_connection {
214            DbConnection::Open(ref conn) => Ok(conn),
215            _ => unreachable!("We just set to Open, this should be impossible."),
216        }
217    }
218
219    pub fn update_local_state(&mut self, local_state: LocalTabsInfo) {
220        let num_tabs = local_state.tabs.len();
221        self.local_tabs.borrow_mut().replace(local_state);
222        info!("update_local_state has {num_tabs} tab entries");
223    }
224
225    pub fn get_remote_tabs(&mut self) -> Option<Vec<ClientRemoteTabs>> {
226        let conn = match self.open_if_exists() {
227            Err(e) => {
228                error_support::report_error!(
229                    "tabs-read-remote",
230                    "Failed to read remote tabs: {}",
231                    e
232                );
233                return None;
234            }
235            Ok(None) => return None,
236            Ok(Some(conn)) => conn,
237        };
238
239        let records: Vec<(TabsRecord, ServerTimestamp)> = match conn.query_rows_and_then_cached(
240            "SELECT record, last_modified FROM tabs",
241            [],
242            |row| -> Result<_> {
243                Ok((
244                    serde_json::from_str(&row.get::<_, String>(0)?)?,
245                    ServerTimestamp(row.get::<_, i64>(1)?),
246                ))
247            },
248        ) {
249            Ok(records) => records,
250            Err(e) => {
251                error_support::report_error!("tabs-read-remote", "Failed to read database: {}", e);
252                return None;
253            }
254        };
255        let mut crts: Vec<ClientRemoteTabs> = Vec::new();
256        let remote_clients: HashMap<String, RemoteClient> =
257            match self.get_meta::<String>(schema::REMOTE_CLIENTS_KEY) {
258                Err(e) => {
259                    error_support::report_error!(
260                        "tabs-read-remote",
261                        "Failed to get remote clients: {}",
262                        e
263                    );
264                    return None;
265                }
266                // We don't return early here since we still store tabs even if we don't
267                // "know" about the client it's associated with (incase it becomes available later)
268                Ok(None) => HashMap::default(),
269                Ok(Some(json)) => serde_json::from_str(&json).unwrap(),
270            };
271        for (record, last_modified) in records {
272            let id = record.id.clone();
273            if let Some(remote_client) = remote_clients.get(&id) {
274                crts.push(ClientRemoteTabs::from_record(
275                    remote_client
276                        .fxa_device_id
277                        .as_ref()
278                        .unwrap_or(&id)
279                        .to_owned(),
280                    last_modified,
281                    remote_client,
282                    record,
283                ));
284            } else {
285                // As per https://github.com/mozilla/application-services/issues/5199, we don't want to store these.
286                warn!("Dropping tabs from an unknown client: {id}");
287            };
288        }
289        // Filter out any tabs the user requested to be closed on other devices but those devices
290        // have not yet actually closed the tab, so we hide them from the user until such time
291        // Should we add a flag here to give the call an option of not doing this?
292        let filtered_crts = self.filter_pending_remote_tabs(crts);
293        Some(filtered_crts)
294    }
295
296    fn filter_pending_remote_tabs(&mut self, crts: Vec<ClientRemoteTabs>) -> Vec<ClientRemoteTabs> {
297        let conn = match self.open_if_exists() {
298            Err(e) => {
299                error_support::report_error!(
300                    "tabs-read-remote",
301                    "Failed to read remote tabs: {}",
302                    e
303                );
304                return crts;
305            }
306            Ok(None) => return crts,
307            Ok(Some(conn)) => conn,
308        };
309        let pending_tabs_result: Result<Vec<(String, String)>> = conn.query_rows_and_then_cached(
310            "SELECT device_id, url
311             FROM remote_tab_commands
312             WHERE command = :command_close_tab",
313            rusqlite::named_params! { ":command_close_tab": CommandKind::CloseTab },
314            |row| {
315                Ok((
316                    row.get::<_, String>(0)?, // device_id
317                    row.get::<_, String>(1)?, // url
318                ))
319            },
320        );
321        // Make a hash map of all urls per client_id that we potentially want to filter
322        let pending_closures = match pending_tabs_result {
323            Ok(pending_closures) => pending_closures.into_iter().fold(
324                HashMap::new(),
325                |mut acc: HashMap<String, Vec<String>>, (device_id, url)| {
326                    acc.entry(device_id).or_default().push(url);
327                    acc
328                },
329            ),
330            Err(e) => {
331                error_support::report_error!("tabs-read-remote", "Failed to read database: {}", e);
332                return crts;
333            }
334        };
335        // Check if any of the client records that were passed in have urls that the user closed
336        // This means that they requested to close those tabs but those devices have not yet got
337        // actually closed the tabs
338        let filtered_crts: Vec<ClientRemoteTabs> = crts
339            .into_iter()
340            .map(|mut crt| {
341                crt.remote_tabs.retain(|tab| {
342                    // The top level in the url_history is the "active" tab, which we should use
343                    // TODO: probably not the best way to url check
344                    !pending_closures
345                        .get(&crt.client_id)
346                        .is_some_and(|urls| urls.contains(&tab.url_history[0]))
347                });
348                crt
349            })
350            .collect();
351        // Return the filtered crts
352        filtered_crts
353    }
354
355    // Keep DB from growing infinitely since we only ask for records since our last sync
356    // and may or may not know about the client it's associated with -- but we could at some point
357    // and should start returning those tabs immediately. If that client hasn't been seen in 3 weeks,
358    // we remove it until it reconnects
359    pub fn remove_stale_clients(&mut self) -> Result<()> {
360        let last_sync = self.get_meta::<i64>(schema::LAST_SYNC_META_KEY)?;
361        if let Some(conn) = self.open_if_exists()? {
362            if let Some(last_sync) = last_sync {
363                let client_ttl_ms = (TABS_CLIENT_TTL as i64) * 1000;
364                // On desktop, a quick write temporarily sets the last_sync to FAR_FUTURE
365                // but if it doesn't set it back to the original (crash, etc) it
366                // means we'll most likely trash all our records (as it's more than any TTL we'd ever do)
367                // so we need to detect this for now until we have native quick write support
368                if last_sync - client_ttl_ms >= 0 && last_sync != (FAR_FUTURE * 1000) {
369                    let tx = conn.unchecked_transaction()?;
370                    let num_removed = tx.execute_cached(
371                        "DELETE FROM tabs WHERE last_modified <= :last_sync - :ttl",
372                        rusqlite::named_params! {
373                            ":last_sync": last_sync,
374                            ":ttl": client_ttl_ms,
375                        },
376                    )?;
377                    info!(
378                        "removed {} stale clients (threshold was {})",
379                        num_removed,
380                        last_sync - client_ttl_ms
381                    );
382                    tx.commit()?;
383                }
384            }
385        }
386        Ok(())
387    }
388
389    pub(crate) fn replace_remote_tabs(
390        &mut self,
391        // This is a tuple because we need to know what the server reports
392        // as the last time a record was modified
393        new_remote_tabs: &Vec<(TabsRecord, ServerTimestamp)>,
394    ) -> Result<()> {
395        let connection = self.open_or_create()?;
396        let tx = connection.unchecked_transaction()?;
397
398        // For tabs it's fine if we override the existing tabs for a remote
399        // there can only ever be one record for each client
400        for remote_tab in new_remote_tabs {
401            let record = &remote_tab.0;
402            let last_modified = remote_tab.1;
403            info!(
404                "inserting tab for device {}, last modified at {}",
405                record.id,
406                last_modified.as_millis()
407            );
408            tx.execute_cached(
409                "INSERT OR REPLACE INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
410                rusqlite::named_params! {
411                    ":guid": &record.id,
412                    ":record": serde_json::to_string(&record).expect("tabs don't fail to serialize"),
413                    ":last_modified": last_modified.as_millis()
414                },
415            )?;
416        }
417        tx.commit()?;
418        Ok(())
419    }
420
421    pub(crate) fn wipe_remote_tabs(&mut self) -> Result<()> {
422        if let Some(db) = self.open_if_exists()? {
423            db.execute_batch("DELETE FROM tabs")?;
424        }
425        Ok(())
426    }
427
428    pub(crate) fn wipe_local_tabs(&self) {
429        self.local_tabs.replace(None);
430    }
431
432    pub(crate) fn put_meta(&mut self, key: &str, value: &dyn ToSql) -> Result<()> {
433        let db = self.open_or_create()?;
434        db.execute_cached(
435            "REPLACE INTO moz_meta (key, value) VALUES (:key, :value)",
436            &[(":key", &key as &dyn ToSql), (":value", value)],
437        )?;
438        Ok(())
439    }
440
441    pub(crate) fn get_meta<T: FromSql>(&mut self, key: &str) -> Result<Option<T>> {
442        match self.open_if_exists() {
443            Ok(Some(db)) => {
444                let res = db.try_query_one(
445                    "SELECT value FROM moz_meta WHERE key = :key",
446                    &[(":key", &key)],
447                    true,
448                )?;
449                Ok(res)
450            }
451            Err(e) => Err(e),
452            Ok(None) => Ok(None),
453        }
454    }
455
456    pub(crate) fn delete_meta(&mut self, key: &str) -> Result<()> {
457        if let Some(db) = self.open_if_exists()? {
458            db.execute_cached("DELETE FROM moz_meta WHERE key = :key", &[(":key", &key)])?;
459        }
460        Ok(())
461    }
462}
463
464// Implementations related to storage of remotely closing remote tabs.
465// We should probably split this module!
466impl TabsStorage {
467    /// Store tabs that we requested to close on other devices but
468    /// not yet executed on target device, other calls like getAll()
469    /// will check against this table to filter out any urls
470    pub fn add_remote_tab_command(
471        &mut self,
472        device_id: &str,
473        command: &RemoteCommand,
474    ) -> Result<bool> {
475        self.add_remote_tab_command_at(device_id, command, Timestamp::now())
476    }
477
478    pub fn add_remote_tab_command_at(
479        &mut self,
480        device_id: &str,
481        command: &RemoteCommand,
482        time_requested: Timestamp,
483    ) -> Result<bool> {
484        let connection = self.open_or_create()?;
485        let RemoteCommand::CloseTab { url } = command;
486        info!("Adding remote command for {device_id} at {time_requested}");
487        trace!("command is {command:?}");
488        // tx maybe not needed for single write?
489        let tx = connection.unchecked_transaction()?;
490        let changes = tx.execute_cached(
491            "INSERT OR IGNORE INTO remote_tab_commands
492                (device_id, command, url, time_requested, time_sent)
493            VALUES (:device_id, :command, :url, :time_requested, null)",
494            rusqlite::named_params! {
495                ":device_id": &device_id,
496                ":url": url,
497                ":time_requested": time_requested,
498                ":command": command.as_ref(),
499            },
500        )?;
501        tx.commit()?;
502        Ok(changes != 0)
503    }
504
505    pub fn remove_remote_tab_command(
506        &mut self,
507        device_id: &str,
508        command: &RemoteCommand,
509    ) -> Result<bool> {
510        let connection = self.open_or_create()?;
511        let RemoteCommand::CloseTab { url } = command;
512        info!("removing remote tab close details: client={device_id}");
513        let tx = connection.unchecked_transaction()?;
514        let changes = tx.execute_cached(
515            "DELETE FROM remote_tab_commands
516             WHERE device_id = :device_id AND command = :command AND url = :url;",
517            rusqlite::named_params! {
518                ":device_id": &device_id,
519                ":url": url,
520                ":command": command.as_ref(),
521            },
522        )?;
523        tx.commit()?;
524        Ok(changes != 0)
525    }
526
527    pub fn get_unsent_commands(&mut self) -> Result<Vec<PendingCommand>> {
528        self.do_get_pending_commands("WHERE time_sent IS NULL")
529    }
530
531    fn do_get_pending_commands(&mut self, where_clause: &str) -> Result<Vec<PendingCommand>> {
532        let Some(conn) = self.open_if_exists()? else {
533            return Ok(Vec::new());
534        };
535        let result = conn.query_rows_and_then_cached(
536            &format!(
537                "SELECT device_id, command, url, time_requested, time_sent
538                    FROM remote_tab_commands
539                    {where_clause}
540                    ORDER BY time_requested
541                    LIMIT 1000 -- sue me!"
542            ),
543            [],
544            |row| -> Result<_> {
545                // overly cautious I guess - ignore bad enum values rather than failing
546                let command = match row.get::<_, CommandKind>(1) {
547                    Ok(c) => c,
548                    Err(e) => {
549                        error!("do_get_pending_commands: ignoring error fetching command: {e:?}");
550                        return Ok(None);
551                    }
552                };
553                Ok(Some(match command {
554                    CommandKind::CloseTab => PendingCommand {
555                        device_id: row.get::<_, String>(0)?,
556                        command: RemoteCommand::CloseTab {
557                            url: row.get::<_, String>(2)?,
558                        },
559                        time_requested: row.get::<_, Timestamp>(3)?,
560                        time_sent: row.get::<_, Option<Timestamp>>(4)?,
561                    },
562                }))
563            },
564        );
565        Ok(match result {
566            Ok(records) => records.into_iter().flatten().collect(),
567            Err(e) => {
568                error_support::report_error!("tabs-get_unsent", "Failed to read database: {}", e);
569                Vec::new()
570            }
571        })
572    }
573
574    pub fn set_pending_command_sent(&mut self, command: &PendingCommand) -> Result<bool> {
575        let connection = self.open_or_create()?;
576        let RemoteCommand::CloseTab { url } = &command.command;
577        info!("setting remote tab sent: client={}", command.device_id);
578        trace!("command: {command:?}");
579        let tx = connection.unchecked_transaction()?;
580        let ts = Timestamp::now();
581        let changes = tx.execute_cached(
582            "UPDATE remote_tab_commands
583             SET time_sent = :ts
584             WHERE device_id = :device_id AND command = :command AND url = :url;",
585            rusqlite::named_params! {
586                ":command": command.command.as_ref(),
587                ":device_id": &command.device_id,
588                ":url": url,
589                ":ts": &ts,
590            },
591        )?;
592        tx.commit()?;
593        Ok(changes != 0)
594    }
595
596    // Remove any pending tabs that are 24hrs older than the last time that client has synced
597    // Or that client's incoming tabs does not have those tabs anymore
598    pub fn remove_old_pending_closures(
599        &mut self,
600        // This is a tuple because we need to know what the server reports
601        // as the last time a record was modified
602        new_remote_tabs: &[(TabsRecord, ServerTimestamp)],
603    ) -> Result<()> {
604        // we need to load our map of client-id -> RemoteClient so we can use the
605        // fxa device ID and not the sync client id.
606        let remote_clients: HashMap<String, RemoteClient> = {
607            match self.get_meta::<String>(schema::REMOTE_CLIENTS_KEY)? {
608                None => HashMap::default(),
609                Some(json) => serde_json::from_str(&json).unwrap(),
610            }
611        };
612
613        let conn = self.open_or_create()?;
614        let tx = conn.unchecked_transaction()?;
615
616        // Insert new remote tabs into a temporary table
617        conn.execute(
618            "CREATE TEMP TABLE if not exists new_remote_tabs (device_id TEXT, url TEXT)",
619            [],
620        )?;
621        conn.execute("DELETE FROM new_remote_tabs", [])?; // Clear previous entries
622
623        for (record, _) in new_remote_tabs.iter() {
624            let fxa_id = remote_clients
625                .get(&record.id)
626                .and_then(|r| r.fxa_device_id.as_ref())
627                .unwrap_or(&record.id);
628            for tab in &record.tabs {
629                if let Some(url) = tab.url_history.first() {
630                    conn.execute(
631                        "INSERT INTO new_remote_tabs (device_id, url) VALUES (?, ?)",
632                        rusqlite::params![fxa_id, url],
633                    )?;
634                }
635            }
636        }
637
638        // Delete entries from pending closures that do not exist in the new remote tabs
639        let delete_sql = "
640         DELETE FROM remote_tab_commands
641         WHERE
642            (device_id IN (SELECT device_id from new_remote_tabs))
643         AND
644         (
645            url NOT IN (
646            SELECT url from new_remote_tabs
647            WHERE new_remote_tabs.device_id = device_id
648            AND :command_close_tab = remote_tab_commands.command)
649         )";
650        conn.execute(
651            delete_sql,
652            rusqlite::named_params! {
653                ":command_close_tab": CommandKind::CloseTab,
654            },
655        )?;
656
657        info!(
658            "deleted {} pending tab closures because they were not in the new tabs",
659            conn.changes()
660        );
661
662        // Anything that couldn't be removed above and is older than REMOTE_COMMAND_TTL_MS
663        // is assumed not closeable and we can remove it from the list
664        let sql = format!("
665            DELETE FROM remote_tab_commands
666            WHERE device_id IN (
667                SELECT guid FROM tabs
668            ) AND (SELECT last_modified FROM tabs WHERE guid = device_id) - time_requested >= {REMOTE_COMMAND_TTL_MS}
669        ");
670        tx.execute_cached(&sql, [])?;
671        info!("deleted {} records because they timed out", conn.changes());
672
673        // Commit changes and clean up temp
674        tx.commit()?;
675        conn.execute("DROP TABLE new_remote_tabs", [])?;
676        Ok(())
677    }
678}
679
680// Simple enum for the DB.
681#[derive(Debug, Copy, Clone)]
682#[repr(u8)]
683enum CommandKind {
684    CloseTab = 0,
685}
686
687impl AsRef<CommandKind> for RemoteCommand {
688    // Required method
689    fn as_ref(&self) -> &CommandKind {
690        match self {
691            RemoteCommand::CloseTab { .. } => &CommandKind::CloseTab,
692        }
693    }
694}
695
696impl FromSql for CommandKind {
697    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
698        Ok(match value.as_i64()? {
699            0 => CommandKind::CloseTab,
700            _ => return Err(FromSqlError::InvalidType),
701        })
702    }
703}
704
705impl ToSql for CommandKind {
706    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
707        Ok(ToSqlOutput::from(*self as u8))
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use std::time::Duration;
714
715    use super::*;
716    use crate::{sync::record::TabsRecordTab, PendingCommand};
717
718    impl RemoteCommand {
719        fn close_tab(url: &str) -> Self {
720            RemoteCommand::CloseTab {
721                url: url.to_string(),
722            }
723        }
724    }
725
726    #[test]
727    fn test_open_if_exists_no_file() {
728        error_support::init_for_tests();
729        let dir = tempfile::tempdir().unwrap();
730        let db_name = dir.path().join("test_open_for_read_no_file.db");
731        let mut storage = TabsStorage::new(db_name.clone());
732        assert!(storage.open_if_exists().unwrap().is_none());
733        storage.open_or_create().unwrap(); // will have created it.
734                                           // make a new storage, but leave the file alone.
735        let mut storage = TabsStorage::new(db_name);
736        // db file exists, so opening for read should open it.
737        assert!(storage.open_if_exists().unwrap().is_some());
738    }
739
740    #[test]
741    fn test_tabs_meta() {
742        error_support::init_for_tests();
743        let dir = tempfile::tempdir().unwrap();
744        let db_name = dir.path().join("test_tabs_meta.db");
745        let mut db = TabsStorage::new(db_name);
746        let test_key = "TEST KEY A";
747        let test_value = "TEST VALUE A";
748        let test_key2 = "TEST KEY B";
749        let test_value2 = "TEST VALUE B";
750
751        // should automatically make the DB if one doesn't exist
752        db.put_meta(test_key, &test_value).unwrap();
753        db.put_meta(test_key2, &test_value2).unwrap();
754
755        let retrieved_value: String = db.get_meta(test_key).unwrap().expect("test value");
756        let retrieved_value2: String = db.get_meta(test_key2).unwrap().expect("test value 2");
757
758        assert_eq!(retrieved_value, test_value);
759        assert_eq!(retrieved_value2, test_value2);
760
761        // check that the value of an existing key can be updated
762        let test_value3 = "TEST VALUE C";
763        db.put_meta(test_key, &test_value3).unwrap();
764
765        let retrieved_value3: String = db.get_meta(test_key).unwrap().expect("test value 3");
766
767        assert_eq!(retrieved_value3, test_value3);
768
769        // check that a deleted key is not retrieved
770        db.delete_meta(test_key).unwrap();
771        let retrieved_value4: Option<String> = db.get_meta(test_key).unwrap();
772        assert!(retrieved_value4.is_none());
773    }
774
775    // Helper struct to model what's stored in the DB
776    struct TabsSQLRecord {
777        guid: String,
778        record: TabsRecord,
779        last_modified: i64,
780    }
781    #[test]
782    fn test_remove_stale_clients() {
783        error_support::init_for_tests();
784        let dir = tempfile::tempdir().unwrap();
785        let db_name = dir.path().join("test_remove_stale_clients.db");
786        let mut storage = TabsStorage::new(db_name);
787        storage.open_or_create().unwrap();
788        assert!(storage.open_if_exists().unwrap().is_some());
789
790        // info about the remote clients.
791        let recent_clients = HashMap::from([
792            (
793                "device-1".to_string(),
794                RemoteClient {
795                    fxa_device_id: None,
796                    device_name: "my device".to_string(),
797                    device_type: sync15::DeviceType::Unknown,
798                },
799            ),
800            (
801                "device-outdated".to_string(),
802                RemoteClient {
803                    fxa_device_id: None,
804                    device_name: "device with no tabs".to_string(),
805                    device_type: DeviceType::Unknown,
806                },
807            ),
808        ]);
809        storage
810            .put_meta(
811                schema::REMOTE_CLIENTS_KEY,
812                &serde_json::to_string(&recent_clients).unwrap(),
813            )
814            .unwrap();
815
816        let records = vec![
817            TabsSQLRecord {
818                guid: "device-1".to_string(),
819                record: TabsRecord {
820                    id: "device-1".to_string(),
821                    ..Default::default()
822                },
823                last_modified: 1643764207000,
824            },
825            TabsSQLRecord {
826                guid: "device-outdated".to_string(),
827                record: TabsRecord {
828                    id: "device-outdated".to_string(),
829                    client_name: "Device outdated".to_string(),
830                    ..Default::default()
831                },
832                last_modified: 1443764207000, // old
833            },
834        ];
835        let db = storage.open_if_exists().unwrap().unwrap();
836        for record in records {
837            db.execute(
838                "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
839                rusqlite::named_params! {
840                    ":guid": &record.guid,
841                    ":record": serde_json::to_string(&record.record).unwrap(),
842                    ":last_modified": &record.last_modified,
843                },
844            ).unwrap();
845        }
846        // pretend we just synced
847        let last_synced = 1643764207000_i64;
848        storage
849            .put_meta(schema::LAST_SYNC_META_KEY, &last_synced)
850            .unwrap();
851        storage.remove_stale_clients().unwrap();
852
853        let remote_tabs = storage.get_remote_tabs().unwrap();
854        // We should've removed the outdated device
855        assert_eq!(remote_tabs.len(), 1);
856        // Assert the correct record is still being returned
857        assert_eq!(remote_tabs[0].client_id, "device-1");
858    }
859
860    fn pending_url_command(device_id: &str, url: &str, ts: Timestamp) -> PendingCommand {
861        PendingCommand {
862            device_id: device_id.to_string(),
863            command: RemoteCommand::CloseTab {
864                url: url.to_string(),
865            },
866            time_requested: ts,
867            time_sent: None,
868        }
869    }
870
871    #[test]
872    fn test_add_pending_dupe_simple() {
873        error_support::init_for_tests();
874        let mut storage = TabsStorage::new_with_mem_path("test_add_pending_dupe_simple");
875        let command = RemoteCommand::close_tab("https://example1.com");
876        // returns a bool to say if it's new or not.
877        assert!(storage
878            .add_remote_tab_command("device-1", &command)
879            .expect("should work"));
880        assert!(!storage
881            .add_remote_tab_command("device-1", &command)
882            .expect("should work"));
883        assert!(storage
884            .remove_remote_tab_command("device-1", &command)
885            .expect("should work"));
886        assert!(storage
887            .add_remote_tab_command("device-1", &command)
888            .expect("should work"));
889    }
890
891    #[test]
892    fn test_add_pending_remote_close() {
893        error_support::init_for_tests();
894        let mut storage = TabsStorage::new_with_mem_path("test_add_pending_remote_close");
895        storage.open_or_create().unwrap();
896        assert!(storage.open_if_exists().unwrap().is_some());
897
898        let now = Timestamp::now();
899        let earliest = now.checked_sub(Duration::from_millis(1)).unwrap();
900        let later = now.checked_add(Duration::from_millis(1)).unwrap();
901        let latest = now.checked_add(Duration::from_millis(2)).unwrap();
902        // The tabs requested to to be closed. We'll insert them in the "wrong" order
903        // relative to their time-stamp.
904        storage
905            .add_remote_tab_command_at(
906                "device-1",
907                &RemoteCommand::close_tab("https://example1.com"),
908                latest,
909            )
910            .expect("should work");
911        storage
912            .add_remote_tab_command_at(
913                "device-1",
914                &RemoteCommand::close_tab("https://example2.com"),
915                earliest,
916            )
917            .expect("should work");
918        storage
919            .add_remote_tab_command_at(
920                "device-2",
921                &RemoteCommand::close_tab("https://example2.com"),
922                now,
923            )
924            .expect("should work");
925        storage
926            .add_remote_tab_command_at(
927                "device-2",
928                &RemoteCommand::close_tab("https://example3.com"),
929                later,
930            )
931            .expect("should work");
932
933        let got = storage.get_unsent_commands().unwrap();
934
935        assert_eq!(got.len(), 4);
936        assert_eq!(
937            got,
938            vec![
939                pending_url_command("device-1", "https://example2.com", earliest),
940                pending_url_command("device-2", "https://example2.com", now),
941                pending_url_command("device-2", "https://example3.com", later),
942                pending_url_command("device-1", "https://example1.com", latest),
943            ]
944        );
945    }
946
947    #[test]
948    fn test_remote_tabs_filters_pending_closures() {
949        error_support::init_for_tests();
950        let mut storage =
951            TabsStorage::new_with_mem_path("test_remote_tabs_filters_pending_closures");
952        let records = vec![
953            TabsSQLRecord {
954                guid: "device-1".to_string(),
955                record: TabsRecord {
956                    id: "device-1".to_string(),
957                    client_name: "Device #1".to_string(),
958                    tabs: vec![TabsRecordTab {
959                        title: "the title".to_string(),
960                        url_history: vec!["https://mozilla.org/".to_string()],
961                        icon: Some("https://mozilla.org/icon".to_string()),
962                        last_used: 1711929600015, // 4/1/2024
963                        ..Default::default()
964                    }],
965                    ..Default::default()
966                },
967                last_modified: 1711929600015, // 4/1/2024
968            },
969            TabsSQLRecord {
970                guid: "device-2".to_string(),
971                record: TabsRecord {
972                    id: "device-2".to_string(),
973                    client_name: "Another device".to_string(),
974                    tabs: vec![
975                        TabsRecordTab {
976                            title: "the title".to_string(),
977                            url_history: vec!["https://mozilla.org/".to_string()],
978                            icon: Some("https://mozilla.org/icon".to_string()),
979                            last_used: 1711929600015, // 4/1/2024
980                            ..Default::default()
981                        },
982                        TabsRecordTab {
983                            title: "the title".to_string(),
984                            url_history: vec![
985                                "https://example.com/".to_string(),
986                                "https://example1.com/".to_string(),
987                            ],
988                            icon: None,
989                            last_used: 1711929600015, // 4/1/2024
990                            ..Default::default()
991                        },
992                        TabsRecordTab {
993                            title: "the title".to_string(),
994                            url_history: vec!["https://example1.com/".to_string()],
995                            icon: None,
996                            last_used: 1711929600015, // 4/1/2024
997                            ..Default::default()
998                        },
999                    ],
1000                    ..Default::default()
1001                },
1002                last_modified: 1711929600015, // 4/1/2024
1003            },
1004        ];
1005
1006        // info about the remote clients.
1007        let recent_clients = HashMap::from([
1008            (
1009                "device-1".to_string(),
1010                RemoteClient {
1011                    fxa_device_id: None,
1012                    device_name: "my device".to_string(),
1013                    device_type: sync15::DeviceType::Unknown,
1014                },
1015            ),
1016            (
1017                "device-2".to_string(),
1018                RemoteClient {
1019                    fxa_device_id: None,
1020                    device_name: "device with no tabs".to_string(),
1021                    device_type: DeviceType::Unknown,
1022                },
1023            ),
1024        ]);
1025        storage
1026            .put_meta(
1027                schema::REMOTE_CLIENTS_KEY,
1028                &serde_json::to_string(&recent_clients).unwrap(),
1029            )
1030            .unwrap();
1031
1032        let db = storage.open_if_exists().unwrap().unwrap();
1033        for record in records {
1034            db.execute(
1035                "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
1036                rusqlite::named_params! {
1037                    ":guid": &record.guid,
1038                    ":record": serde_json::to_string(&record.record).unwrap(),
1039                    ":last_modified": &record.last_modified,
1040                },
1041            ).unwrap();
1042        }
1043
1044        // Some tabs were requested to be closed
1045        storage
1046            .add_remote_tab_command(
1047                "device-1",
1048                &RemoteCommand::close_tab("https://mozilla.org/"),
1049            )
1050            .unwrap();
1051        storage
1052            .add_remote_tab_command(
1053                "device-2",
1054                &RemoteCommand::close_tab("https://example.com/"),
1055            )
1056            .unwrap();
1057        storage
1058            .add_remote_tab_command(
1059                "device-2",
1060                &RemoteCommand::close_tab("https://example1.com/"),
1061            )
1062            .unwrap();
1063
1064        let remote_tabs = storage.get_remote_tabs().unwrap();
1065
1066        assert_eq!(remote_tabs.len(), 2);
1067
1068        // Device 1 had only 1 tab synced, we remotely closed it, so we expect no tabs
1069        assert_eq!(remote_tabs[0].client_id, "device-1");
1070        assert_eq!(remote_tabs[0].remote_tabs.len(), 0);
1071
1072        // Device 2 had 3 tabs open and we remotely closed 2, so we expect 1 tab returned
1073        assert_eq!(remote_tabs[1].client_id, "device-2");
1074        assert_eq!(remote_tabs[1].remote_tabs.len(), 1);
1075        assert_eq!(
1076            remote_tabs[1].remote_tabs[0],
1077            RemoteTab {
1078                title: "the title".to_string(),
1079                url_history: vec!["https://mozilla.org/".to_string()],
1080                icon: Some("https://mozilla.org/icon".to_string()),
1081                last_used: 1711929600015000, //server time is ns, so 1000 bigger than local.
1082                ..Default::default()
1083            }
1084        );
1085    }
1086
1087    #[test]
1088    fn test_remove_old_pending_closures_timed_removal() {
1089        error_support::init_for_tests();
1090        let mut storage =
1091            TabsStorage::new_with_mem_path("test_remove_old_pending_closures_timed_removal");
1092
1093        let now = Timestamp::now();
1094        let older = now
1095            .checked_sub(Duration::from_millis(REMOTE_COMMAND_TTL_MS))
1096            .unwrap();
1097
1098        {
1099            let db = storage.open_if_exists().unwrap().unwrap();
1100
1101            // We manually insert two devices, one that hasn't updated in awhile and one that's
1102            // updated recently
1103            db.execute(
1104                "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-synced', '', :now);",
1105                rusqlite::named_params! {
1106                    ":now" : now,
1107                },
1108            )
1109            .unwrap();
1110
1111            db.execute(
1112                "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-not-synced', '', :old);",
1113                    rusqlite::named_params! {
1114                        ":old" : older,
1115                    },
1116            ).unwrap();
1117        }
1118        // We also manually insert some pending remote tab closures, we specifically add a recent one
1119        // and one that is 48hrs older since that device updated, which should get removed
1120        storage
1121            .add_remote_tab_command_at(
1122                "device-synced",
1123                &RemoteCommand::close_tab("https://example.com"),
1124                older,
1125            )
1126            .unwrap();
1127
1128        storage
1129            .add_remote_tab_command_at(
1130                "device-not-synced",
1131                &RemoteCommand::close_tab("https://example2.com"),
1132                now,
1133            )
1134            .unwrap();
1135
1136        {
1137            let db = storage.open_if_exists().unwrap().unwrap();
1138
1139            // Verify we actually have 2 pending closures
1140            let before_count: i64 = db
1141                .conn_ext_query_one("SELECT COUNT(*) FROM remote_tab_commands")
1142                .unwrap();
1143            assert_eq!(before_count, 2);
1144        }
1145        // "incoming" records from other devices
1146        let new_records = vec![(
1147            TabsRecord {
1148                id: "device-not-synced".to_string(),
1149                tabs: vec![TabsRecordTab {
1150                    url_history: vec!["https://example2.com".to_string()],
1151                    ..Default::default()
1152                }],
1153                ..Default::default()
1154            },
1155            ServerTimestamp::from_millis(now.as_millis_i64()),
1156        )];
1157        // Cleanup old pending closures
1158        storage.remove_old_pending_closures(&new_records).unwrap();
1159
1160        let reopen_db = storage.open_if_exists().unwrap().unwrap();
1161        let after_count: i64 = reopen_db
1162            .conn_ext_query_one("SELECT COUNT(*) FROM remote_tab_commands")
1163            .unwrap();
1164        assert_eq!(after_count, 1);
1165
1166        let remaining_device_id: String = reopen_db
1167            .conn_ext_query_one("SELECT device_id FROM remote_tab_commands")
1168            .unwrap();
1169
1170        // Only the device that still hasn't synced keeps
1171        assert_eq!(remaining_device_id, "device-not-synced");
1172    }
1173    #[test]
1174    fn test_remove_old_pending_closures_no_tab_removal() {
1175        error_support::init_for_tests();
1176        let mut storage =
1177            TabsStorage::new_with_mem_path("test_remove_old_pending_closures_no_tab_removal");
1178        let db = storage.open_if_exists().unwrap().unwrap();
1179
1180        let now_ms: u64 = Timestamp::now().as_millis();
1181
1182        // Set up the initial state with tabs that have been synced recently
1183        db.execute(
1184            "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-recent', '', :now);",
1185            rusqlite::named_params! {
1186                ":now": now_ms,
1187            },
1188        )
1189        .unwrap();
1190
1191        // Insert pending closures for a device
1192        db.execute(
1193        "INSERT INTO remote_tab_commands (device_id, command, url, time_requested) VALUES (:device_id, :command, :url, :time_requested)",
1194        rusqlite::named_params! {
1195            ":command": CommandKind::CloseTab,
1196            ":device_id": "device-recent",
1197            ":url": "https://example.com",
1198            ":time_requested": now_ms,
1199        },
1200    ).unwrap();
1201
1202        db.execute(
1203        "INSERT INTO remote_tab_commands (device_id, command, url, time_requested) VALUES (:device_id, :command, :url, :time_requested)",
1204        rusqlite::named_params! {
1205            ":command": CommandKind::CloseTab,
1206            ":device_id": "device-recent",
1207            ":url": "https://old-url.com",
1208            ":time_requested": now_ms,
1209        },
1210    ).unwrap();
1211
1212        // Verify initial state has 2 pending closures
1213        let before_count: i64 = db
1214            .query_row("SELECT COUNT(*) FROM remote_tab_commands", [], |row| {
1215                row.get(0)
1216            })
1217            .unwrap();
1218        assert_eq!(before_count, 2);
1219
1220        // Simulate incoming data that no longer includes one of the URLs
1221        let new_records = vec![(
1222            TabsRecord {
1223                id: "device-recent".to_string(),
1224                tabs: vec![
1225                    TabsRecordTab {
1226                        url_history: vec!["https://example99.com".to_string()],
1227                        ..Default::default()
1228                    },
1229                    TabsRecordTab {
1230                        url_history: vec!["https://example.com".to_string()],
1231                        ..Default::default()
1232                    },
1233                ],
1234                ..Default::default()
1235            },
1236            ServerTimestamp::default(),
1237        )];
1238
1239        // Perform the cleanup
1240        storage.remove_old_pending_closures(&new_records).unwrap();
1241
1242        // need to reopen db to avoid mutable errors
1243        let reopen_db = storage.open_if_exists().unwrap().unwrap();
1244        // Check results after cleanup
1245        let after_count: i64 = reopen_db
1246            .query_row("SELECT COUNT(*) FROM remote_tab_commands", [], |row| {
1247                row.get(0)
1248            })
1249            .unwrap();
1250        assert_eq!(after_count, 1); // Only one entry should remain
1251
1252        let remaining_url: String = reopen_db
1253            .query_row("SELECT url FROM remote_tab_commands", [], |row| row.get(0))
1254            .unwrap();
1255
1256        assert_eq!(remaining_url, "https://example.com"); // The URL still present in new_records should remain
1257    }
1258
1259    #[test]
1260    fn test_remove_pending_command() {
1261        error_support::init_for_tests();
1262        let mut storage = TabsStorage::new_with_mem_path("test_remove_pending_command");
1263        storage.open_or_create().unwrap();
1264        assert!(storage.open_if_exists().unwrap().is_some());
1265
1266        storage
1267            .add_remote_tab_command(
1268                "device-1",
1269                &RemoteCommand::close_tab("https://example1.com"),
1270            )
1271            .expect("should work");
1272
1273        assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1274        assert!(!storage
1275            .remove_remote_tab_command(
1276                "no-devce",
1277                &RemoteCommand::close_tab("https://example1.com"),
1278            )
1279            .unwrap());
1280        assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1281
1282        assert!(!storage
1283            .remove_remote_tab_command(
1284                "device-1",
1285                &RemoteCommand::close_tab("https://example9.com"),
1286            )
1287            .unwrap());
1288        assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1289
1290        assert!(storage
1291            .remove_remote_tab_command(
1292                "device-1",
1293                &RemoteCommand::close_tab("https://example1.com"),
1294            )
1295            .unwrap());
1296        assert_eq!(storage.get_unsent_commands().unwrap().len(), 0);
1297    }
1298
1299    #[test]
1300    fn test_sent_command() {
1301        error_support::init_for_tests();
1302        let mut storage = TabsStorage::new_with_mem_path("test_sent_command");
1303        let command = RemoteCommand::close_tab("https://example1.com");
1304        storage
1305            .add_remote_tab_command("device-1", &command)
1306            .expect("should work");
1307
1308        assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1309        let pending_command = PendingCommand {
1310            device_id: "device-1".to_string(),
1311            command: command.clone(),
1312            time_requested: Timestamp::now(),
1313            time_sent: None,
1314        };
1315        assert!(storage.set_pending_command_sent(&pending_command).unwrap());
1316        assert_eq!(storage.get_unsent_commands().unwrap().len(), 0);
1317        // but can't re-add it because it's still alive.
1318        assert!(!storage
1319            .add_remote_tab_command("device-1", &command)
1320            .unwrap());
1321        // can remove it.
1322        assert!(storage
1323            .remove_remote_tab_command("device-1", &command)
1324            .unwrap());
1325        // now can re-add it.
1326        assert!(storage
1327            .add_remote_tab_command("device-1", &command)
1328            .unwrap());
1329        assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1330    }
1331
1332    #[test]
1333    fn test_remove_pending_closures_only_affects_target_device() {
1334        error_support::init_for_tests();
1335        let mut storage =
1336            TabsStorage::new_with_mem_path("test_remove_pending_closures_target_device");
1337        let now = Timestamp::now();
1338
1339        let db = storage.open_if_exists().unwrap().unwrap();
1340
1341        // Insert two devices into the tabs db
1342        db.execute(
1343            "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-1', '', :now);",
1344            rusqlite::named_params! { ":now" : now },
1345        )
1346        .unwrap();
1347
1348        db.execute(
1349            "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-2', '', :now);",
1350            rusqlite::named_params! { ":now" : now },
1351        )
1352        .unwrap();
1353
1354        // Add three commands, two for device-1 and one for device-2
1355        storage
1356            .add_remote_tab_command(
1357                "device-1",
1358                &RemoteCommand::close_tab("https://example1.com"),
1359            )
1360            .unwrap();
1361
1362        storage
1363            .add_remote_tab_command(
1364                "device-1",
1365                &RemoteCommand::close_tab("https://example2.com"),
1366            )
1367            .unwrap();
1368
1369        storage
1370            .add_remote_tab_command(
1371                "device-2",
1372                &RemoteCommand::close_tab("https://example3.com"),
1373            )
1374            .unwrap();
1375
1376        // Pretend only device-1 "synced", example2.com tab was closed
1377        let new_records = vec![(
1378            TabsRecord {
1379                id: "device-1".to_string(),
1380                tabs: vec![TabsRecordTab {
1381                    url_history: vec!["https://example1.com".to_string()],
1382                    ..Default::default()
1383                }],
1384                ..Default::default()
1385            },
1386            ServerTimestamp::default(),
1387        )];
1388
1389        storage.remove_old_pending_closures(&new_records).unwrap();
1390
1391        let reopen_db = storage.open_if_exists().unwrap().unwrap();
1392        let remaining_commands: Vec<(String, String)> = reopen_db
1393            .prepare("SELECT device_id, url FROM remote_tab_commands")
1394            .unwrap()
1395            .query_map([], |row| {
1396                Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1397            })
1398            .unwrap()
1399            .collect::<rusqlite::Result<Vec<_>, _>>()
1400            .unwrap();
1401        // We should only have removed 1 command from the list
1402        assert_eq!(remaining_commands.len(), 2);
1403        assert!(remaining_commands
1404            .contains(&("device-1".to_string(), "https://example1.com".to_string())));
1405        assert!(remaining_commands
1406            .contains(&("device-2".to_string(), "https://example3.com".to_string())));
1407    }
1408
1409    #[test]
1410    fn test_close_connection() {
1411        let dir = tempfile::tempdir().unwrap();
1412        let db_path = dir.path().join("test_close_connection.db");
1413        let mut storage = TabsStorage::new(db_path);
1414
1415        // Open the connection
1416        storage.open_or_create().unwrap();
1417
1418        // Verify that the connection is open
1419        assert!(matches!(storage.db_connection, DbConnection::Open(_)));
1420
1421        // Close the connection
1422        storage.close();
1423
1424        // Verify that the connection is closed
1425        assert!(matches!(storage.db_connection, DbConnection::Closed));
1426
1427        // Attempt to reopen the connection should fail
1428        let result = storage.open_or_create();
1429        assert!(result.is_err());
1430        assert!(matches!(
1431            result.unwrap_err(),
1432            Error::UnexpectedConnectionState
1433        ));
1434    }
1435}