1const URI_LENGTH_MAX: usize = 65536;
7const TAB_ENTRIES_LIMIT: usize = 5;
9
10const REMOTE_COMMAND_TTL_MS: u64 = 2 * 24 * 60 * 60 * 1000; use crate::error::*;
16use crate::schema;
17use crate::sync::record::TabsRecord;
18use crate::DeviceType;
19use crate::{PendingCommand, RemoteCommand, Timestamp};
20use error_support::{error, info, trace, warn};
21use rusqlite::{
22 types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef},
23 Connection, OpenFlags,
24};
25use serde_derive::{Deserialize, Serialize};
26use sql_support::open_database::{self, open_database_with_flags};
27use sql_support::ConnExt;
28use std::cell::RefCell;
29use std::collections::HashMap;
30use std::path::{Path, PathBuf};
31use sync15::{RemoteClient, ServerTimestamp};
32pub type TabsDeviceType = crate::DeviceType;
33pub type RemoteTabRecord = RemoteTab;
34
35pub(crate) const TABS_CLIENT_TTL: u32 = 15_552_000; const FAR_FUTURE: i64 = 4_102_405_200_000; const MAX_PAYLOAD_SIZE: usize = 512 * 1024; const MAX_TITLE_CHAR_LENGTH: usize = 512; #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
41pub struct RemoteTab {
42 pub title: String,
43 pub url_history: Vec<String>,
44 pub icon: Option<String>,
45 pub last_used: i64, pub inactive: bool,
47}
48
49#[derive(Clone, Debug)]
50pub struct ClientRemoteTabs {
51 pub client_id: String,
55 pub client_name: String,
56 pub device_type: DeviceType,
57 pub last_modified: i64,
58 pub remote_tabs: Vec<RemoteTab>,
59}
60
61pub(crate) enum DbConnection {
62 Created,
63 Open(Connection),
64 Closed,
65}
66
67pub struct TabsStorage {
80 local_tabs: RefCell<Option<Vec<RemoteTab>>>,
81 db_path: PathBuf,
82 db_connection: DbConnection,
83}
84
85impl TabsStorage {
86 pub fn new(db_path: impl AsRef<Path>) -> Self {
87 Self {
88 local_tabs: RefCell::default(),
89 db_path: db_path.as_ref().to_path_buf(),
90 db_connection: DbConnection::Created,
91 }
92 }
93
94 pub fn close(&mut self) {
95 if let DbConnection::Open(conn) =
96 std::mem::replace(&mut self.db_connection, DbConnection::Closed)
97 {
98 if let Err(err) = conn.close() {
99 error!("Failed to close the connection: {:?}", err);
101 }
102 }
103 }
104
105 pub fn new_with_mem_path(db_path: &str) -> Self {
108 let name = PathBuf::from(format!("file:{}?mode=memory&cache=shared", db_path));
109 Self::new(name)
110 }
111
112 pub fn open_if_exists(&mut self) -> Result<Option<&Connection>> {
114 match self.db_connection {
115 DbConnection::Open(ref conn) => return Ok(Some(conn)),
116 DbConnection::Closed => return Ok(None),
117 DbConnection::Created => {}
118 }
119 let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
120 | OpenFlags::SQLITE_OPEN_URI
121 | OpenFlags::SQLITE_OPEN_READ_WRITE;
122 match open_database_with_flags(
123 self.db_path.clone(),
124 flags,
125 &crate::schema::TabsMigrationLogic,
126 ) {
127 Ok(conn) => {
128 info!("tabs storage is opening an existing database");
129 self.db_connection = DbConnection::Open(conn);
130 match self.db_connection {
131 DbConnection::Open(ref conn) => Ok(Some(conn)),
132 _ => unreachable!("impossible value"),
133 }
134 }
135 Err(open_database::Error::SqlError(rusqlite::Error::SqliteFailure(code, _)))
136 if code.code == rusqlite::ErrorCode::CannotOpen =>
137 {
138 info!("tabs storage could not open an existing database and hasn't been asked to create one");
139 Ok(None)
140 }
141 Err(e) => Err(e.into()),
142 }
143 }
144
145 pub fn open_or_create(&mut self) -> Result<&Connection> {
147 match self.db_connection {
148 DbConnection::Open(ref conn) => return Ok(conn),
149 DbConnection::Closed => return Err(Error::UnexpectedConnectionState),
150 DbConnection::Created => {}
151 }
152 let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
153 | OpenFlags::SQLITE_OPEN_URI
154 | OpenFlags::SQLITE_OPEN_READ_WRITE
155 | OpenFlags::SQLITE_OPEN_CREATE;
156 let conn = open_database_with_flags(
157 self.db_path.clone(),
158 flags,
159 &crate::schema::TabsMigrationLogic,
160 )?;
161 info!("tabs storage is creating a database connection");
162 self.db_connection = DbConnection::Open(conn);
163 match self.db_connection {
164 DbConnection::Open(ref conn) => Ok(conn),
165 _ => unreachable!("We just set to Open, this should be impossible."),
166 }
167 }
168
169 pub fn update_local_state(&mut self, local_state: Vec<RemoteTab>) {
170 let num_tabs = local_state.len();
171 self.local_tabs.borrow_mut().replace(local_state);
172 info!("update_local_state has {num_tabs} tab entries");
173 }
174
175 pub fn prepare_local_tabs_for_upload(&self) -> Option<Vec<RemoteTab>> {
179 if let Some(local_tabs) = self.local_tabs.borrow().as_ref() {
180 let mut sanitized_tabs: Vec<RemoteTab> = local_tabs
181 .iter()
182 .cloned()
183 .filter_map(|mut tab| {
184 if tab.url_history.is_empty() || !is_url_syncable(&tab.url_history[0]) {
185 return None;
186 }
187 let mut sanitized_history = Vec::with_capacity(TAB_ENTRIES_LIMIT);
188 for url in tab.url_history {
189 if sanitized_history.len() == TAB_ENTRIES_LIMIT {
190 break;
191 }
192 if is_url_syncable(&url) {
193 sanitized_history.push(url);
194 }
195 }
196
197 tab.url_history = sanitized_history;
198 tab.title = slice_up_to(tab.title, MAX_TITLE_CHAR_LENGTH);
200 Some(tab)
201 })
202 .collect();
203 sanitized_tabs.sort_by(|a, b| b.last_used.cmp(&a.last_used));
205 trim_tabs_length(&mut sanitized_tabs, MAX_PAYLOAD_SIZE);
206 info!(
207 "prepare_local_tabs_for_upload found {} tabs",
208 sanitized_tabs.len()
209 );
210 return Some(sanitized_tabs);
211 }
212 warn!("prepare_local_tabs_for_upload - have no local tabs");
215 None
216 }
217
218 pub fn get_remote_tabs(&mut self) -> Option<Vec<ClientRemoteTabs>> {
219 let conn = match self.open_if_exists() {
220 Err(e) => {
221 error_support::report_error!(
222 "tabs-read-remote",
223 "Failed to read remote tabs: {}",
224 e
225 );
226 return None;
227 }
228 Ok(None) => return None,
229 Ok(Some(conn)) => conn,
230 };
231
232 let records: Vec<(TabsRecord, ServerTimestamp)> = match conn.query_rows_and_then_cached(
233 "SELECT record, last_modified FROM tabs",
234 [],
235 |row| -> Result<_> {
236 Ok((
237 serde_json::from_str(&row.get::<_, String>(0)?)?,
238 ServerTimestamp(row.get::<_, i64>(1)?),
239 ))
240 },
241 ) {
242 Ok(records) => records,
243 Err(e) => {
244 error_support::report_error!("tabs-read-remote", "Failed to read database: {}", e);
245 return None;
246 }
247 };
248 let mut crts: Vec<ClientRemoteTabs> = Vec::new();
249 let remote_clients: HashMap<String, RemoteClient> =
250 match self.get_meta::<String>(schema::REMOTE_CLIENTS_KEY) {
251 Err(e) => {
252 error_support::report_error!(
253 "tabs-read-remote",
254 "Failed to get remote clients: {}",
255 e
256 );
257 return None;
258 }
259 Ok(None) => HashMap::default(),
262 Ok(Some(json)) => serde_json::from_str(&json).unwrap(),
263 };
264 for (record, last_modified) in records {
265 let id = record.id.clone();
266 let crt = if let Some(remote_client) = remote_clients.get(&id) {
267 ClientRemoteTabs::from_record_with_remote_client(
268 remote_client
269 .fxa_device_id
270 .as_ref()
271 .unwrap_or(&id)
272 .to_owned(),
273 last_modified,
274 remote_client,
275 record,
276 )
277 } else {
278 info!(
284 "Storing tabs from a client that doesn't appear in the devices list: {}",
285 id,
286 );
287 ClientRemoteTabs::from_record(id, last_modified, record)
288 };
289 crts.push(crt);
290 }
291 let filtered_crts = self.filter_pending_remote_tabs(crts);
295 Some(filtered_crts)
296 }
297
298 fn filter_pending_remote_tabs(&mut self, crts: Vec<ClientRemoteTabs>) -> Vec<ClientRemoteTabs> {
299 let conn = match self.open_if_exists() {
300 Err(e) => {
301 error_support::report_error!(
302 "tabs-read-remote",
303 "Failed to read remote tabs: {}",
304 e
305 );
306 return crts;
307 }
308 Ok(None) => return crts,
309 Ok(Some(conn)) => conn,
310 };
311 let pending_tabs_result: Result<Vec<(String, String)>> = conn.query_rows_and_then_cached(
312 "SELECT device_id, url
313 FROM remote_tab_commands
314 WHERE command = :command_close_tab",
315 rusqlite::named_params! { ":command_close_tab": CommandKind::CloseTab },
316 |row| {
317 Ok((
318 row.get::<_, String>(0)?, row.get::<_, String>(1)?, ))
321 },
322 );
323 let pending_closures = match pending_tabs_result {
325 Ok(pending_closures) => pending_closures.into_iter().fold(
326 HashMap::new(),
327 |mut acc: HashMap<String, Vec<String>>, (device_id, url)| {
328 acc.entry(device_id).or_default().push(url);
329 acc
330 },
331 ),
332 Err(e) => {
333 error_support::report_error!("tabs-read-remote", "Failed to read database: {}", e);
334 return crts;
335 }
336 };
337 let filtered_crts: Vec<ClientRemoteTabs> = crts
341 .into_iter()
342 .map(|mut crt| {
343 crt.remote_tabs.retain(|tab| {
344 !pending_closures
347 .get(&crt.client_id)
348 .is_some_and(|urls| urls.contains(&tab.url_history[0]))
349 });
350 crt
351 })
352 .collect();
353 filtered_crts
355 }
356
357 pub fn remove_stale_clients(&mut self) -> Result<()> {
362 let last_sync = self.get_meta::<i64>(schema::LAST_SYNC_META_KEY)?;
363 if let Some(conn) = self.open_if_exists()? {
364 if let Some(last_sync) = last_sync {
365 let client_ttl_ms = (TABS_CLIENT_TTL as i64) * 1000;
366 if last_sync - client_ttl_ms >= 0 && last_sync != (FAR_FUTURE * 1000) {
371 let tx = conn.unchecked_transaction()?;
372 let num_removed = tx.execute_cached(
373 "DELETE FROM tabs WHERE last_modified <= :last_sync - :ttl",
374 rusqlite::named_params! {
375 ":last_sync": last_sync,
376 ":ttl": client_ttl_ms,
377 },
378 )?;
379 info!(
380 "removed {} stale clients (threshold was {})",
381 num_removed,
382 last_sync - client_ttl_ms
383 );
384 tx.commit()?;
385 }
386 }
387 }
388 Ok(())
389 }
390
391 pub(crate) fn replace_remote_tabs(
392 &mut self,
393 new_remote_tabs: &Vec<(TabsRecord, ServerTimestamp)>,
396 ) -> Result<()> {
397 let connection = self.open_or_create()?;
398 let tx = connection.unchecked_transaction()?;
399
400 for remote_tab in new_remote_tabs {
403 let record = &remote_tab.0;
404 let last_modified = remote_tab.1;
405 info!(
406 "inserting tab for device {}, last modified at {}",
407 record.id,
408 last_modified.as_millis()
409 );
410 tx.execute_cached(
411 "INSERT OR REPLACE INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
412 rusqlite::named_params! {
413 ":guid": &record.id,
414 ":record": serde_json::to_string(&record).expect("tabs don't fail to serialize"),
415 ":last_modified": last_modified.as_millis()
416 },
417 )?;
418 }
419 tx.commit()?;
420 Ok(())
421 }
422
423 pub(crate) fn wipe_remote_tabs(&mut self) -> Result<()> {
424 if let Some(db) = self.open_if_exists()? {
425 db.execute_batch("DELETE FROM tabs")?;
426 }
427 Ok(())
428 }
429
430 pub(crate) fn wipe_local_tabs(&self) {
431 self.local_tabs.replace(None);
432 }
433
434 pub(crate) fn put_meta(&mut self, key: &str, value: &dyn ToSql) -> Result<()> {
435 let db = self.open_or_create()?;
436 db.execute_cached(
437 "REPLACE INTO moz_meta (key, value) VALUES (:key, :value)",
438 &[(":key", &key as &dyn ToSql), (":value", value)],
439 )?;
440 Ok(())
441 }
442
443 pub(crate) fn get_meta<T: FromSql>(&mut self, key: &str) -> Result<Option<T>> {
444 match self.open_if_exists() {
445 Ok(Some(db)) => {
446 let res = db.try_query_one(
447 "SELECT value FROM moz_meta WHERE key = :key",
448 &[(":key", &key)],
449 true,
450 )?;
451 Ok(res)
452 }
453 Err(e) => Err(e),
454 Ok(None) => Ok(None),
455 }
456 }
457
458 pub(crate) fn delete_meta(&mut self, key: &str) -> Result<()> {
459 if let Some(db) = self.open_if_exists()? {
460 db.execute_cached("DELETE FROM moz_meta WHERE key = :key", &[(":key", &key)])?;
461 }
462 Ok(())
463 }
464}
465
466impl TabsStorage {
469 pub fn add_remote_tab_command(
473 &mut self,
474 device_id: &str,
475 command: &RemoteCommand,
476 ) -> Result<bool> {
477 self.add_remote_tab_command_at(device_id, command, Timestamp::now())
478 }
479
480 pub fn add_remote_tab_command_at(
481 &mut self,
482 device_id: &str,
483 command: &RemoteCommand,
484 time_requested: Timestamp,
485 ) -> Result<bool> {
486 let connection = self.open_or_create()?;
487 let RemoteCommand::CloseTab { url } = command;
488 info!("Adding remote command for {device_id} at {time_requested}");
489 trace!("command is {command:?}");
490 let tx = connection.unchecked_transaction()?;
492 let changes = tx.execute_cached(
493 "INSERT OR IGNORE INTO remote_tab_commands
494 (device_id, command, url, time_requested, time_sent)
495 VALUES (:device_id, :command, :url, :time_requested, null)",
496 rusqlite::named_params! {
497 ":device_id": &device_id,
498 ":url": url,
499 ":time_requested": time_requested,
500 ":command": command.as_ref(),
501 },
502 )?;
503 tx.commit()?;
504 Ok(changes != 0)
505 }
506
507 pub fn remove_remote_tab_command(
508 &mut self,
509 device_id: &str,
510 command: &RemoteCommand,
511 ) -> Result<bool> {
512 let connection = self.open_or_create()?;
513 let RemoteCommand::CloseTab { url } = command;
514 info!("removing remote tab close details: client={device_id}");
515 let tx = connection.unchecked_transaction()?;
516 let changes = tx.execute_cached(
517 "DELETE FROM remote_tab_commands
518 WHERE device_id = :device_id AND command = :command AND url = :url;",
519 rusqlite::named_params! {
520 ":device_id": &device_id,
521 ":url": url,
522 ":command": command.as_ref(),
523 },
524 )?;
525 tx.commit()?;
526 Ok(changes != 0)
527 }
528
529 pub fn get_unsent_commands(&mut self) -> Result<Vec<PendingCommand>> {
530 self.do_get_pending_commands("WHERE time_sent IS NULL")
531 }
532
533 fn do_get_pending_commands(&mut self, where_clause: &str) -> Result<Vec<PendingCommand>> {
534 let Some(conn) = self.open_if_exists()? else {
535 return Ok(Vec::new());
536 };
537 let result = conn.query_rows_and_then_cached(
538 &format!(
539 "SELECT device_id, command, url, time_requested, time_sent
540 FROM remote_tab_commands
541 {where_clause}
542 ORDER BY time_requested
543 LIMIT 1000 -- sue me!"
544 ),
545 [],
546 |row| -> Result<_> {
547 let command = match row.get::<_, CommandKind>(1) {
549 Ok(c) => c,
550 Err(e) => {
551 error!("do_get_pending_commands: ignoring error fetching command: {e:?}");
552 return Ok(None);
553 }
554 };
555 Ok(Some(match command {
556 CommandKind::CloseTab => PendingCommand {
557 device_id: row.get::<_, String>(0)?,
558 command: RemoteCommand::CloseTab {
559 url: row.get::<_, String>(2)?,
560 },
561 time_requested: row.get::<_, Timestamp>(3)?,
562 time_sent: row.get::<_, Option<Timestamp>>(4)?,
563 },
564 }))
565 },
566 );
567 Ok(match result {
568 Ok(records) => records.into_iter().flatten().collect(),
569 Err(e) => {
570 error_support::report_error!("tabs-get_unsent", "Failed to read database: {}", e);
571 Vec::new()
572 }
573 })
574 }
575
576 pub fn set_pending_command_sent(&mut self, command: &PendingCommand) -> Result<bool> {
577 let connection = self.open_or_create()?;
578 let RemoteCommand::CloseTab { url } = &command.command;
579 info!("setting remote tab sent: client={}", command.device_id);
580 trace!("command: {command:?}");
581 let tx = connection.unchecked_transaction()?;
582 let ts = Timestamp::now();
583 let changes = tx.execute_cached(
584 "UPDATE remote_tab_commands
585 SET time_sent = :ts
586 WHERE device_id = :device_id AND command = :command AND url = :url;",
587 rusqlite::named_params! {
588 ":command": command.command.as_ref(),
589 ":device_id": &command.device_id,
590 ":url": url,
591 ":ts": &ts,
592 },
593 )?;
594 tx.commit()?;
595 Ok(changes != 0)
596 }
597
598 pub fn remove_old_pending_closures(
601 &mut self,
602 new_remote_tabs: &[(TabsRecord, ServerTimestamp)],
605 ) -> Result<()> {
606 let remote_clients: HashMap<String, RemoteClient> = {
609 match self.get_meta::<String>(schema::REMOTE_CLIENTS_KEY)? {
610 None => HashMap::default(),
611 Some(json) => serde_json::from_str(&json).unwrap(),
612 }
613 };
614
615 let conn = self.open_or_create()?;
616 let tx = conn.unchecked_transaction()?;
617
618 conn.execute(
620 "CREATE TEMP TABLE if not exists new_remote_tabs (device_id TEXT, url TEXT)",
621 [],
622 )?;
623 conn.execute("DELETE FROM new_remote_tabs", [])?; for (record, _) in new_remote_tabs.iter() {
626 let fxa_id = remote_clients
627 .get(&record.id)
628 .and_then(|r| r.fxa_device_id.as_ref())
629 .unwrap_or(&record.id);
630 for tab in &record.tabs {
631 if let Some(url) = tab.url_history.first() {
632 conn.execute(
633 "INSERT INTO new_remote_tabs (device_id, url) VALUES (?, ?)",
634 rusqlite::params![fxa_id, url],
635 )?;
636 }
637 }
638 }
639
640 let delete_sql = "
642 DELETE FROM remote_tab_commands
643 WHERE
644 (device_id IN (SELECT device_id from new_remote_tabs))
645 AND
646 (
647 url NOT IN (
648 SELECT url from new_remote_tabs
649 WHERE new_remote_tabs.device_id = device_id
650 AND :command_close_tab = remote_tab_commands.command)
651 )";
652 conn.execute(
653 delete_sql,
654 rusqlite::named_params! {
655 ":command_close_tab": CommandKind::CloseTab,
656 },
657 )?;
658
659 info!(
660 "deleted {} pending tab closures because they were not in the new tabs",
661 conn.changes()
662 );
663
664 let sql = format!("
667 DELETE FROM remote_tab_commands
668 WHERE device_id IN (
669 SELECT guid FROM tabs
670 ) AND (SELECT last_modified FROM tabs WHERE guid = device_id) - time_requested >= {REMOTE_COMMAND_TTL_MS}
671 ");
672 tx.execute_cached(&sql, [])?;
673 info!("deleted {} records because they timed out", conn.changes());
674
675 tx.commit()?;
677 conn.execute("DROP TABLE new_remote_tabs", [])?;
678 Ok(())
679 }
680}
681
682#[derive(Debug, Copy, Clone)]
684#[repr(u8)]
685enum CommandKind {
686 CloseTab = 0,
687}
688
689impl AsRef<CommandKind> for RemoteCommand {
690 fn as_ref(&self) -> &CommandKind {
692 match self {
693 RemoteCommand::CloseTab { .. } => &CommandKind::CloseTab,
694 }
695 }
696}
697
698impl FromSql for CommandKind {
699 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
700 Ok(match value.as_i64()? {
701 0 => CommandKind::CloseTab,
702 _ => return Err(FromSqlError::InvalidType),
703 })
704 }
705}
706
707impl ToSql for CommandKind {
708 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
709 Ok(ToSqlOutput::from(*self as u8))
710 }
711}
712
713fn trim_tabs_length(tabs: &mut Vec<RemoteTab>, payload_size_max_bytes: usize) {
716 if let Some(count) = payload_support::try_fit_items(tabs, payload_size_max_bytes).as_some() {
717 tabs.truncate(count.get());
718 }
719}
720
721pub fn slice_up_to(s: String, max_len: usize) -> String {
726 if max_len >= s.len() {
727 return s;
728 }
729
730 let ellipsis = '\u{2026}';
731 let mut idx = max_len - ellipsis.len_utf8();
733 while !s.is_char_boundary(idx) {
734 idx -= 1;
735 }
736 let mut new_str = s[..idx].to_string();
737 new_str.push(ellipsis);
738 new_str
739}
740
741fn is_url_syncable(url: &str) -> bool {
743 url.len() <= URI_LENGTH_MAX
744 && !(url.starts_with("about:")
745 || url.starts_with("resource:")
746 || url.starts_with("chrome:")
747 || url.starts_with("wyciwyg:")
748 || url.starts_with("blob:")
749 || url.starts_with("file:")
750 || url.starts_with("moz-extension:")
751 || url.starts_with("data:"))
752}
753
754#[cfg(test)]
755mod tests {
756 use payload_support::compute_serialized_size;
757 use std::time::Duration;
758
759 use super::*;
760 use crate::{sync::record::TabsRecordTab, PendingCommand};
761
762 impl RemoteCommand {
763 fn close_tab(url: &str) -> Self {
764 RemoteCommand::CloseTab {
765 url: url.to_string(),
766 }
767 }
768 }
769
770 #[test]
771 fn test_is_url_syncable() {
772 assert!(is_url_syncable("https://bobo.com"));
773 assert!(is_url_syncable("ftp://bobo.com"));
774 assert!(!is_url_syncable("about:blank"));
775 assert!(is_url_syncable("aboutbobo.com"));
777 assert!(!is_url_syncable("file:///Users/eoger/bobo"));
778 }
779
780 #[test]
781 fn test_open_if_exists_no_file() {
782 error_support::init_for_tests();
783 let dir = tempfile::tempdir().unwrap();
784 let db_name = dir.path().join("test_open_for_read_no_file.db");
785 let mut storage = TabsStorage::new(db_name.clone());
786 assert!(storage.open_if_exists().unwrap().is_none());
787 storage.open_or_create().unwrap(); let mut storage = TabsStorage::new(db_name);
790 assert!(storage.open_if_exists().unwrap().is_some());
792 }
793
794 #[test]
795 fn test_tabs_meta() {
796 error_support::init_for_tests();
797 let dir = tempfile::tempdir().unwrap();
798 let db_name = dir.path().join("test_tabs_meta.db");
799 let mut db = TabsStorage::new(db_name);
800 let test_key = "TEST KEY A";
801 let test_value = "TEST VALUE A";
802 let test_key2 = "TEST KEY B";
803 let test_value2 = "TEST VALUE B";
804
805 db.put_meta(test_key, &test_value).unwrap();
807 db.put_meta(test_key2, &test_value2).unwrap();
808
809 let retrieved_value: String = db.get_meta(test_key).unwrap().expect("test value");
810 let retrieved_value2: String = db.get_meta(test_key2).unwrap().expect("test value 2");
811
812 assert_eq!(retrieved_value, test_value);
813 assert_eq!(retrieved_value2, test_value2);
814
815 let test_value3 = "TEST VALUE C";
817 db.put_meta(test_key, &test_value3).unwrap();
818
819 let retrieved_value3: String = db.get_meta(test_key).unwrap().expect("test value 3");
820
821 assert_eq!(retrieved_value3, test_value3);
822
823 db.delete_meta(test_key).unwrap();
825 let retrieved_value4: Option<String> = db.get_meta(test_key).unwrap();
826 assert!(retrieved_value4.is_none());
827 }
828
829 #[test]
830 fn test_prepare_local_tabs_for_upload() {
831 error_support::init_for_tests();
832 let mut storage = TabsStorage::new_with_mem_path("test_prepare_local_tabs_for_upload");
833 assert_eq!(storage.prepare_local_tabs_for_upload(), None);
834 storage.update_local_state(vec![
835 RemoteTab {
836 url_history: vec!["about:blank".to_owned(), "https://foo.bar".to_owned()],
837 ..Default::default()
838 },
839 RemoteTab {
840 url_history: vec![
841 "https://foo.bar".to_owned(),
842 "about:blank".to_owned(),
843 "about:blank".to_owned(),
844 "about:blank".to_owned(),
845 "about:blank".to_owned(),
846 "about:blank".to_owned(),
847 "about:blank".to_owned(),
848 "about:blank".to_owned(),
849 ],
850 ..Default::default()
851 },
852 RemoteTab {
853 url_history: vec![
854 "https://foo.bar".to_owned(),
855 "about:blank".to_owned(),
856 "https://foo2.bar".to_owned(),
857 "https://foo3.bar".to_owned(),
858 "https://foo4.bar".to_owned(),
859 "https://foo5.bar".to_owned(),
860 "https://foo6.bar".to_owned(),
861 ],
862 ..Default::default()
863 },
864 RemoteTab {
865 ..Default::default()
866 },
867 ]);
868 assert_eq!(
869 storage.prepare_local_tabs_for_upload(),
870 Some(vec![
871 RemoteTab {
872 url_history: vec!["https://foo.bar".to_owned()],
873 ..Default::default()
874 },
875 RemoteTab {
876 url_history: vec![
877 "https://foo.bar".to_owned(),
878 "https://foo2.bar".to_owned(),
879 "https://foo3.bar".to_owned(),
880 "https://foo4.bar".to_owned(),
881 "https://foo5.bar".to_owned()
882 ],
883 ..Default::default()
884 },
885 ])
886 );
887 }
888 #[test]
889 fn test_trimming_tab_title() {
890 error_support::init_for_tests();
891 let mut storage = TabsStorage::new_with_mem_path("test_prepare_local_tabs_for_upload");
892 assert_eq!(storage.prepare_local_tabs_for_upload(), None);
893 storage.update_local_state(vec![RemoteTab {
894 title: "a".repeat(MAX_TITLE_CHAR_LENGTH + 10), url_history: vec!["https://foo.bar".to_owned()],
896 ..Default::default()
897 }]);
898 let ellipsis_char = '\u{2026}';
899 let mut truncated_title = "a".repeat(MAX_TITLE_CHAR_LENGTH - ellipsis_char.len_utf8());
900 truncated_title.push(ellipsis_char);
901 assert_eq!(
902 storage.prepare_local_tabs_for_upload(),
903 Some(vec![
904 RemoteTab {
906 title: truncated_title, url_history: vec!["https://foo.bar".to_owned()],
908 ..Default::default()
909 },
910 ])
911 );
912 }
913 #[test]
914 fn test_utf8_safe_title_trim() {
915 error_support::init_for_tests();
916 let mut storage = TabsStorage::new_with_mem_path("test_prepare_local_tabs_for_upload");
917 assert_eq!(storage.prepare_local_tabs_for_upload(), None);
918 storage.update_local_state(vec![
919 RemoteTab {
920 title: "😍".repeat(MAX_TITLE_CHAR_LENGTH + 10), url_history: vec!["https://foo.bar".to_owned()],
922 ..Default::default()
923 },
924 RemoteTab {
925 title: "を".repeat(MAX_TITLE_CHAR_LENGTH + 5), url_history: vec!["https://foo_jp.bar".to_owned()],
927 ..Default::default()
928 },
929 ]);
930 let ellipsis_char = '\u{2026}';
931 let mut truncated_title = "😍".repeat(127);
933 let mut truncated_jp_title = "を".repeat(169);
935 truncated_title.push(ellipsis_char);
936 truncated_jp_title.push(ellipsis_char);
937 let remote_tabs = storage.prepare_local_tabs_for_upload().unwrap();
938 assert_eq!(
939 remote_tabs,
940 vec![
941 RemoteTab {
942 title: truncated_title, url_history: vec!["https://foo.bar".to_owned()],
944 ..Default::default()
945 },
946 RemoteTab {
947 title: truncated_jp_title, url_history: vec!["https://foo_jp.bar".to_owned()],
949 ..Default::default()
950 },
951 ]
952 );
953 assert!(remote_tabs[0].title.chars().count() <= MAX_TITLE_CHAR_LENGTH);
955 assert!(remote_tabs[1].title.chars().count() <= MAX_TITLE_CHAR_LENGTH);
956 }
957 #[test]
958 fn test_trim_tabs_length() {
959 error_support::init_for_tests();
960 let mut storage = TabsStorage::new_with_mem_path("test_prepare_local_tabs_for_upload");
961 assert_eq!(storage.prepare_local_tabs_for_upload(), None);
962 let mut too_many_tabs: Vec<RemoteTab> = Vec::new();
963 for n in 1..5000 {
964 too_many_tabs.push(RemoteTab {
965 title: "aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa" .to_owned(),
967 url_history: vec![format!("https://foo{}.bar", n)],
968 ..Default::default()
969 });
970 }
971 let tabs_mem_size = compute_serialized_size(&too_many_tabs).unwrap();
972 assert!(tabs_mem_size > MAX_PAYLOAD_SIZE);
974 storage.update_local_state(too_many_tabs.clone());
976 let tabs_to_upload = &storage.prepare_local_tabs_for_upload().unwrap();
978 assert!(compute_serialized_size(tabs_to_upload).unwrap() <= MAX_PAYLOAD_SIZE);
979 }
980 struct TabsSQLRecord {
982 guid: String,
983 record: TabsRecord,
984 last_modified: i64,
985 }
986 #[test]
987 fn test_remove_stale_clients() {
988 error_support::init_for_tests();
989 let dir = tempfile::tempdir().unwrap();
990 let db_name = dir.path().join("test_remove_stale_clients.db");
991 let mut storage = TabsStorage::new(db_name);
992 storage.open_or_create().unwrap();
993 assert!(storage.open_if_exists().unwrap().is_some());
994
995 let records = vec![
996 TabsSQLRecord {
997 guid: "device-1".to_string(),
998 record: TabsRecord {
999 id: "device-1".to_string(),
1000 client_name: "Device #1".to_string(),
1001 tabs: vec![TabsRecordTab {
1002 title: "the title".to_string(),
1003 url_history: vec!["https://mozilla.org/".to_string()],
1004 icon: Some("https://mozilla.org/icon".to_string()),
1005 last_used: 1643764207000,
1006 ..Default::default()
1007 }],
1008 },
1009 last_modified: 1643764207000,
1010 },
1011 TabsSQLRecord {
1012 guid: "device-outdated".to_string(),
1013 record: TabsRecord {
1014 id: "device-outdated".to_string(),
1015 client_name: "Device outdated".to_string(),
1016 tabs: vec![TabsRecordTab {
1017 title: "the title".to_string(),
1018 url_history: vec!["https://mozilla.org/".to_string()],
1019 icon: Some("https://mozilla.org/icon".to_string()),
1020 last_used: 1643764207000,
1021 ..Default::default()
1022 }],
1023 },
1024 last_modified: 1443764207000, },
1026 ];
1027 let db = storage.open_if_exists().unwrap().unwrap();
1028 for record in records {
1029 db.execute(
1030 "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
1031 rusqlite::named_params! {
1032 ":guid": &record.guid,
1033 ":record": serde_json::to_string(&record.record).unwrap(),
1034 ":last_modified": &record.last_modified,
1035 },
1036 ).unwrap();
1037 }
1038 let last_synced = 1643764207000_i64;
1040 storage
1041 .put_meta(schema::LAST_SYNC_META_KEY, &last_synced)
1042 .unwrap();
1043 storage.remove_stale_clients().unwrap();
1044
1045 let remote_tabs = storage.get_remote_tabs().unwrap();
1046 assert_eq!(remote_tabs.len(), 1);
1048 assert_eq!(remote_tabs[0].client_id, "device-1");
1050 }
1051
1052 fn pending_url_command(device_id: &str, url: &str, ts: Timestamp) -> PendingCommand {
1053 PendingCommand {
1054 device_id: device_id.to_string(),
1055 command: RemoteCommand::CloseTab {
1056 url: url.to_string(),
1057 },
1058 time_requested: ts,
1059 time_sent: None,
1060 }
1061 }
1062
1063 #[test]
1064 fn test_add_pending_dupe_simple() {
1065 error_support::init_for_tests();
1066 let mut storage = TabsStorage::new_with_mem_path("test_add_pending_dupe_simple");
1067 let command = RemoteCommand::close_tab("https://example1.com");
1068 assert!(storage
1070 .add_remote_tab_command("device-1", &command)
1071 .expect("should work"));
1072 assert!(!storage
1073 .add_remote_tab_command("device-1", &command)
1074 .expect("should work"));
1075 assert!(storage
1076 .remove_remote_tab_command("device-1", &command)
1077 .expect("should work"));
1078 assert!(storage
1079 .add_remote_tab_command("device-1", &command)
1080 .expect("should work"));
1081 }
1082
1083 #[test]
1084 fn test_add_pending_remote_close() {
1085 error_support::init_for_tests();
1086 let mut storage = TabsStorage::new_with_mem_path("test_add_pending_remote_close");
1087 storage.open_or_create().unwrap();
1088 assert!(storage.open_if_exists().unwrap().is_some());
1089
1090 let now = Timestamp::now();
1091 let earliest = now.checked_sub(Duration::from_millis(1)).unwrap();
1092 let later = now.checked_add(Duration::from_millis(1)).unwrap();
1093 let latest = now.checked_add(Duration::from_millis(2)).unwrap();
1094 storage
1097 .add_remote_tab_command_at(
1098 "device-1",
1099 &RemoteCommand::close_tab("https://example1.com"),
1100 latest,
1101 )
1102 .expect("should work");
1103 storage
1104 .add_remote_tab_command_at(
1105 "device-1",
1106 &RemoteCommand::close_tab("https://example2.com"),
1107 earliest,
1108 )
1109 .expect("should work");
1110 storage
1111 .add_remote_tab_command_at(
1112 "device-2",
1113 &RemoteCommand::close_tab("https://example2.com"),
1114 now,
1115 )
1116 .expect("should work");
1117 storage
1118 .add_remote_tab_command_at(
1119 "device-2",
1120 &RemoteCommand::close_tab("https://example3.com"),
1121 later,
1122 )
1123 .expect("should work");
1124
1125 let got = storage.get_unsent_commands().unwrap();
1126
1127 assert_eq!(got.len(), 4);
1128 assert_eq!(
1129 got,
1130 vec![
1131 pending_url_command("device-1", "https://example2.com", earliest),
1132 pending_url_command("device-2", "https://example2.com", now),
1133 pending_url_command("device-2", "https://example3.com", later),
1134 pending_url_command("device-1", "https://example1.com", latest),
1135 ]
1136 );
1137 }
1138
1139 #[test]
1140 fn test_remote_tabs_filters_pending_closures() {
1141 error_support::init_for_tests();
1142 let mut storage =
1143 TabsStorage::new_with_mem_path("test_remote_tabs_filters_pending_closures");
1144 let records = vec![
1145 TabsSQLRecord {
1146 guid: "device-1".to_string(),
1147 record: TabsRecord {
1148 id: "device-1".to_string(),
1149 client_name: "Device #1".to_string(),
1150 tabs: vec![TabsRecordTab {
1151 title: "the title".to_string(),
1152 url_history: vec!["https://mozilla.org/".to_string()],
1153 icon: Some("https://mozilla.org/icon".to_string()),
1154 last_used: 1711929600015, ..Default::default()
1156 }],
1157 },
1158 last_modified: 1711929600015, },
1160 TabsSQLRecord {
1161 guid: "device-2".to_string(),
1162 record: TabsRecord {
1163 id: "device-2".to_string(),
1164 client_name: "Another device".to_string(),
1165 tabs: vec![
1166 TabsRecordTab {
1167 title: "the title".to_string(),
1168 url_history: vec!["https://mozilla.org/".to_string()],
1169 icon: Some("https://mozilla.org/icon".to_string()),
1170 last_used: 1711929600015, ..Default::default()
1172 },
1173 TabsRecordTab {
1174 title: "the title".to_string(),
1175 url_history: vec![
1176 "https://example.com/".to_string(),
1177 "https://example1.com/".to_string(),
1178 ],
1179 icon: None,
1180 last_used: 1711929600015, ..Default::default()
1182 },
1183 TabsRecordTab {
1184 title: "the title".to_string(),
1185 url_history: vec!["https://example1.com/".to_string()],
1186 icon: None,
1187 last_used: 1711929600015, ..Default::default()
1189 },
1190 ],
1191 },
1192 last_modified: 1711929600015, },
1194 ];
1195
1196 let db = storage.open_if_exists().unwrap().unwrap();
1197 for record in records {
1198 db.execute(
1199 "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
1200 rusqlite::named_params! {
1201 ":guid": &record.guid,
1202 ":record": serde_json::to_string(&record.record).unwrap(),
1203 ":last_modified": &record.last_modified,
1204 },
1205 ).unwrap();
1206 }
1207
1208 storage
1210 .add_remote_tab_command(
1211 "device-1",
1212 &RemoteCommand::close_tab("https://mozilla.org/"),
1213 )
1214 .unwrap();
1215 storage
1216 .add_remote_tab_command(
1217 "device-2",
1218 &RemoteCommand::close_tab("https://example.com/"),
1219 )
1220 .unwrap();
1221 storage
1222 .add_remote_tab_command(
1223 "device-2",
1224 &RemoteCommand::close_tab("https://example1.com/"),
1225 )
1226 .unwrap();
1227
1228 let remote_tabs = storage.get_remote_tabs().unwrap();
1229
1230 assert_eq!(remote_tabs.len(), 2);
1231
1232 assert_eq!(remote_tabs[0].client_id, "device-1");
1234 assert_eq!(remote_tabs[0].remote_tabs.len(), 0);
1235
1236 assert_eq!(remote_tabs[1].client_id, "device-2");
1238 assert_eq!(remote_tabs[1].remote_tabs.len(), 1);
1239 assert_eq!(
1240 remote_tabs[1].remote_tabs[0],
1241 RemoteTab {
1242 title: "the title".to_string(),
1243 url_history: vec!["https://mozilla.org/".to_string()],
1244 icon: Some("https://mozilla.org/icon".to_string()),
1245 last_used: 1711929600015000, ..Default::default()
1247 }
1248 );
1249 }
1250
1251 #[test]
1252 fn test_remove_old_pending_closures_timed_removal() {
1253 error_support::init_for_tests();
1254 let mut storage =
1255 TabsStorage::new_with_mem_path("test_remove_old_pending_closures_timed_removal");
1256
1257 let now = Timestamp::now();
1258 let older = now
1259 .checked_sub(Duration::from_millis(REMOTE_COMMAND_TTL_MS))
1260 .unwrap();
1261
1262 {
1263 let db = storage.open_if_exists().unwrap().unwrap();
1264
1265 db.execute(
1268 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-synced', '', :now);",
1269 rusqlite::named_params! {
1270 ":now" : now,
1271 },
1272 )
1273 .unwrap();
1274
1275 db.execute(
1276 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-not-synced', '', :old);",
1277 rusqlite::named_params! {
1278 ":old" : older,
1279 },
1280 ).unwrap();
1281 }
1282 storage
1285 .add_remote_tab_command_at(
1286 "device-synced",
1287 &RemoteCommand::close_tab("https://example.com"),
1288 older,
1289 )
1290 .unwrap();
1291
1292 storage
1293 .add_remote_tab_command_at(
1294 "device-not-synced",
1295 &RemoteCommand::close_tab("https://example2.com"),
1296 now,
1297 )
1298 .unwrap();
1299
1300 {
1301 let db = storage.open_if_exists().unwrap().unwrap();
1302
1303 let before_count: i64 = db
1305 .query_one("SELECT COUNT(*) FROM remote_tab_commands")
1306 .unwrap();
1307 assert_eq!(before_count, 2);
1308 }
1309 let new_records = vec![(
1311 TabsRecord {
1312 id: "device-not-synced".to_string(),
1313 client_name: "".to_string(),
1314 tabs: vec![TabsRecordTab {
1315 url_history: vec!["https://example2.com".to_string()],
1316 ..Default::default()
1317 }],
1318 },
1319 ServerTimestamp::from_millis(now.as_millis_i64()),
1320 )];
1321 storage.remove_old_pending_closures(&new_records).unwrap();
1323
1324 let reopen_db = storage.open_if_exists().unwrap().unwrap();
1325 let after_count: i64 = reopen_db
1326 .query_one("SELECT COUNT(*) FROM remote_tab_commands")
1327 .unwrap();
1328 assert_eq!(after_count, 1);
1329
1330 let remaining_device_id: String = reopen_db
1331 .query_one("SELECT device_id FROM remote_tab_commands")
1332 .unwrap();
1333
1334 assert_eq!(remaining_device_id, "device-not-synced");
1336 }
1337 #[test]
1338 fn test_remove_old_pending_closures_no_tab_removal() {
1339 error_support::init_for_tests();
1340 let mut storage =
1341 TabsStorage::new_with_mem_path("test_remove_old_pending_closures_no_tab_removal");
1342 let db = storage.open_if_exists().unwrap().unwrap();
1343
1344 let now_ms: u64 = Timestamp::now().as_millis();
1345
1346 db.execute(
1348 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-recent', '', :now);",
1349 rusqlite::named_params! {
1350 ":now": now_ms,
1351 },
1352 )
1353 .unwrap();
1354
1355 db.execute(
1357 "INSERT INTO remote_tab_commands (device_id, command, url, time_requested) VALUES (:device_id, :command, :url, :time_requested)",
1358 rusqlite::named_params! {
1359 ":command": CommandKind::CloseTab,
1360 ":device_id": "device-recent",
1361 ":url": "https://example.com",
1362 ":time_requested": now_ms,
1363 },
1364 ).unwrap();
1365
1366 db.execute(
1367 "INSERT INTO remote_tab_commands (device_id, command, url, time_requested) VALUES (:device_id, :command, :url, :time_requested)",
1368 rusqlite::named_params! {
1369 ":command": CommandKind::CloseTab,
1370 ":device_id": "device-recent",
1371 ":url": "https://old-url.com",
1372 ":time_requested": now_ms,
1373 },
1374 ).unwrap();
1375
1376 let before_count: i64 = db
1378 .query_row("SELECT COUNT(*) FROM remote_tab_commands", [], |row| {
1379 row.get(0)
1380 })
1381 .unwrap();
1382 assert_eq!(before_count, 2);
1383
1384 let new_records = vec![(
1386 TabsRecord {
1387 id: "device-recent".to_string(),
1388 client_name: "".to_string(),
1389 tabs: vec![
1390 TabsRecordTab {
1391 url_history: vec!["https://example99.com".to_string()],
1392 ..Default::default()
1393 },
1394 TabsRecordTab {
1395 url_history: vec!["https://example.com".to_string()],
1396 ..Default::default()
1397 },
1398 ],
1399 },
1400 ServerTimestamp::default(),
1401 )];
1402
1403 storage.remove_old_pending_closures(&new_records).unwrap();
1405
1406 let reopen_db = storage.open_if_exists().unwrap().unwrap();
1408 let after_count: i64 = reopen_db
1410 .query_row("SELECT COUNT(*) FROM remote_tab_commands", [], |row| {
1411 row.get(0)
1412 })
1413 .unwrap();
1414 assert_eq!(after_count, 1); let remaining_url: String = reopen_db
1417 .query_row("SELECT url FROM remote_tab_commands", [], |row| row.get(0))
1418 .unwrap();
1419
1420 assert_eq!(remaining_url, "https://example.com"); }
1422
1423 #[test]
1424 fn test_remove_pending_command() {
1425 error_support::init_for_tests();
1426 let mut storage = TabsStorage::new_with_mem_path("test_remove_pending_command");
1427 storage.open_or_create().unwrap();
1428 assert!(storage.open_if_exists().unwrap().is_some());
1429
1430 storage
1431 .add_remote_tab_command(
1432 "device-1",
1433 &RemoteCommand::close_tab("https://example1.com"),
1434 )
1435 .expect("should work");
1436
1437 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1438 assert!(!storage
1439 .remove_remote_tab_command(
1440 "no-devce",
1441 &RemoteCommand::close_tab("https://example1.com"),
1442 )
1443 .unwrap());
1444 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1445
1446 assert!(!storage
1447 .remove_remote_tab_command(
1448 "device-1",
1449 &RemoteCommand::close_tab("https://example9.com"),
1450 )
1451 .unwrap());
1452 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1453
1454 assert!(storage
1455 .remove_remote_tab_command(
1456 "device-1",
1457 &RemoteCommand::close_tab("https://example1.com"),
1458 )
1459 .unwrap());
1460 assert_eq!(storage.get_unsent_commands().unwrap().len(), 0);
1461 }
1462
1463 #[test]
1464 fn test_sent_command() {
1465 error_support::init_for_tests();
1466 let mut storage = TabsStorage::new_with_mem_path("test_sent_command");
1467 let command = RemoteCommand::close_tab("https://example1.com");
1468 storage
1469 .add_remote_tab_command("device-1", &command)
1470 .expect("should work");
1471
1472 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1473 let pending_command = PendingCommand {
1474 device_id: "device-1".to_string(),
1475 command: command.clone(),
1476 time_requested: Timestamp::now(),
1477 time_sent: None,
1478 };
1479 assert!(storage.set_pending_command_sent(&pending_command).unwrap());
1480 assert_eq!(storage.get_unsent_commands().unwrap().len(), 0);
1481 assert!(!storage
1483 .add_remote_tab_command("device-1", &command)
1484 .unwrap());
1485 assert!(storage
1487 .remove_remote_tab_command("device-1", &command)
1488 .unwrap());
1489 assert!(storage
1491 .add_remote_tab_command("device-1", &command)
1492 .unwrap());
1493 assert_eq!(storage.get_unsent_commands().unwrap().len(), 1);
1494 }
1495
1496 #[test]
1497 fn test_remove_pending_closures_only_affects_target_device() {
1498 error_support::init_for_tests();
1499 let mut storage =
1500 TabsStorage::new_with_mem_path("test_remove_pending_closures_target_device");
1501 let now = Timestamp::now();
1502
1503 let db = storage.open_if_exists().unwrap().unwrap();
1504
1505 db.execute(
1507 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-1', '', :now);",
1508 rusqlite::named_params! { ":now" : now },
1509 )
1510 .unwrap();
1511
1512 db.execute(
1513 "INSERT INTO tabs (guid, record, last_modified) VALUES ('device-2', '', :now);",
1514 rusqlite::named_params! { ":now" : now },
1515 )
1516 .unwrap();
1517
1518 storage
1520 .add_remote_tab_command(
1521 "device-1",
1522 &RemoteCommand::close_tab("https://example1.com"),
1523 )
1524 .unwrap();
1525
1526 storage
1527 .add_remote_tab_command(
1528 "device-1",
1529 &RemoteCommand::close_tab("https://example2.com"),
1530 )
1531 .unwrap();
1532
1533 storage
1534 .add_remote_tab_command(
1535 "device-2",
1536 &RemoteCommand::close_tab("https://example3.com"),
1537 )
1538 .unwrap();
1539
1540 let new_records = vec![(
1542 TabsRecord {
1543 id: "device-1".to_string(),
1544 client_name: "".to_string(),
1545 tabs: vec![TabsRecordTab {
1546 url_history: vec!["https://example1.com".to_string()],
1547 ..Default::default()
1548 }],
1549 },
1550 ServerTimestamp::default(),
1551 )];
1552
1553 storage.remove_old_pending_closures(&new_records).unwrap();
1554
1555 let reopen_db = storage.open_if_exists().unwrap().unwrap();
1556 let remaining_commands: Vec<(String, String)> = reopen_db
1557 .prepare("SELECT device_id, url FROM remote_tab_commands")
1558 .unwrap()
1559 .query_map([], |row| {
1560 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1561 })
1562 .unwrap()
1563 .collect::<rusqlite::Result<Vec<_>, _>>()
1564 .unwrap();
1565 assert_eq!(remaining_commands.len(), 2);
1567 assert!(remaining_commands
1568 .contains(&("device-1".to_string(), "https://example1.com".to_string())));
1569 assert!(remaining_commands
1570 .contains(&("device-2".to_string(), "https://example3.com".to_string())));
1571 }
1572
1573 #[test]
1574 fn test_close_connection() {
1575 let dir = tempfile::tempdir().unwrap();
1576 let db_path = dir.path().join("test_close_connection.db");
1577 let mut storage = TabsStorage::new(db_path);
1578
1579 storage.open_or_create().unwrap();
1581
1582 assert!(matches!(storage.db_connection, DbConnection::Open(_)));
1584
1585 storage.close();
1587
1588 assert!(matches!(storage.db_connection, DbConnection::Closed));
1590
1591 let result = storage.open_or_create();
1593 assert!(result.is_err());
1594 assert!(matches!(
1595 result.unwrap_err(),
1596 Error::UnexpectedConnectionState
1597 ));
1598 }
1599}