Files
servo/components/storage/tests/client_storage.rs
Taym Haddadi 5ac8bf4db3 Implement StorageManager API (#43976)
Add the Storage Standard WebIDL for NavigatorStorage and StorageManager,
wire navigator.storage on Window and Worker, and implement persisted(),
persist(), and estimate().


Testing: covered by WP test.
part of #39100

fixes #39101

---------

Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
2026-04-21 13:41:58 +00:00

306 lines
10 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::path::PathBuf;
use rusqlite::Connection;
use servo_base::generic_channel::{self, GenericCallback};
use servo_base::id::{BrowsingContextId, PipelineNamespace, PipelineNamespaceId, WebViewId};
use servo_url::ServoUrl;
use storage::ClientStorageThreadFactory;
use storage_traits::client_storage::{
ClientStorageThreadHandle, ClientStorageThreadMessage, StorageIdentifier, StorageProxyMap,
StorageType,
};
fn install_test_namespace() {
PipelineNamespace::install(PipelineNamespaceId(1));
}
fn registry_db_path(tmp_dir: &tempfile::TempDir) -> PathBuf {
tmp_dir.path().join("clientstorage").join("reg.sqlite")
}
fn open_registry(tmp_dir: &tempfile::TempDir) -> Connection {
Connection::open(registry_db_path(tmp_dir)).expect("Registry database should exist")
}
fn obtain_bottle_map(
handle: &ClientStorageThreadHandle,
storage_type: StorageType,
webview: WebViewId,
storage_identifier: StorageIdentifier,
origin: servo_url::ImmutableOrigin,
) -> StorageProxyMap {
handle
.obtain_a_storage_bottle_map(storage_type, webview, storage_identifier, origin)
.recv()
.unwrap()
.unwrap()
}
#[test]
fn test_exit() {
let handle: ClientStorageThreadHandle = ClientStorageThreadFactory::new(None);
let (sender, receiver) = generic_channel::channel().unwrap();
handle
.send(ClientStorageThreadMessage::Exit(sender))
.unwrap();
receiver.recv().unwrap();
// Workaround for https://github.com/servo/servo/issues/32912
#[cfg(windows)]
std::thread::sleep(std::time::Duration::from_millis(1000));
}
#[test]
fn test_workflow() {
install_test_namespace();
let tmp_dir = tempfile::tempdir().unwrap();
let handle: ClientStorageThreadHandle =
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
let url = ServoUrl::parse("https://example.com").unwrap();
// Obtain a first storage proxy map.
let storage_proxy_map = handle
.obtain_a_storage_bottle_map(
StorageType::Local,
WebViewId::new(servo_base::id::TEST_PAINTER_ID),
StorageIdentifier::IndexedDB,
url.origin(),
)
.recv()
.unwrap()
.unwrap();
// Create a db.
let receiver = handle.create_database(storage_proxy_map.bottle_id, "test1".to_string());
let path = receiver.recv().unwrap().expect("Path should be created");
assert!(std::fs::read_dir(path.clone()).is_ok());
// Create another db with the same name.
let receiver = handle.create_database(storage_proxy_map.bottle_id, "test1".to_string());
assert!(receiver.recv().unwrap().is_err());
// Create another db with a different same.
let receiver = handle.create_database(storage_proxy_map.bottle_id, "test2".to_string());
let yet_another_path = receiver.recv().unwrap().expect("Path should be created");
assert_ne!(path, yet_another_path);
// Delete the dbs.
let receiver = handle.delete_database(storage_proxy_map.bottle_id, "test1".to_string());
receiver.recv().unwrap().expect("Db should be deleted");
let receiver = handle.delete_database(storage_proxy_map.bottle_id, "test2".to_string());
receiver.recv().unwrap().expect("Db should be deleted");
assert!(std::fs::read_dir(path).is_err());
// Get another proxy map fro the same parameters.
let second_proxy_map = handle
.obtain_a_storage_bottle_map(
StorageType::Local,
WebViewId::new(servo_base::id::TEST_PAINTER_ID),
StorageIdentifier::IndexedDB,
url.origin(),
)
.recv()
.unwrap()
.unwrap();
// Bottle id should be the same, because the manager returned the existing id.
assert_eq!(second_proxy_map.bottle_id, storage_proxy_map.bottle_id);
// Workaround for https://github.com/servo/servo/issues/32912
#[cfg(windows)]
std::thread::sleep(std::time::Duration::from_millis(1000));
}
#[test]
fn test_repeated_local_obtain_reuses_same_logical_rows() {
install_test_namespace();
let tmp_dir = tempfile::tempdir().unwrap();
let handle: ClientStorageThreadHandle =
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
let origin = ServoUrl::parse("https://example.com").unwrap().origin();
let webview = WebViewId::new(servo_base::id::TEST_PAINTER_ID);
let first = obtain_bottle_map(
&handle,
StorageType::Local,
webview,
StorageIdentifier::IndexedDB,
origin.clone(),
);
let second = obtain_bottle_map(
&handle,
StorageType::Local,
webview,
StorageIdentifier::IndexedDB,
origin.clone(),
);
assert_eq!(first.bottle_id, second.bottle_id);
let registry = open_registry(&tmp_dir);
let local_shed_count: i64 = registry
.query_row(
"SELECT COUNT(*) FROM sheds WHERE storage_type = 'local' AND browsing_context IS NULL;",
[],
|row| row.get(0),
)
.unwrap();
let shelf_count: i64 = registry
.query_row(
"SELECT COUNT(*) FROM shelves WHERE origin = ?1;",
[origin.ascii_serialization()],
|row| row.get(0),
)
.unwrap();
let bucket_count: i64 = registry
.query_row("SELECT COUNT(*) FROM buckets;", [], |row| row.get(0))
.unwrap();
let bottle_count: i64 = registry
.query_row("SELECT COUNT(*) FROM bottles;", [], |row| row.get(0))
.unwrap();
assert_eq!(local_shed_count, 1);
assert_eq!(shelf_count, 1);
assert_eq!(bucket_count, 1);
assert_eq!(bottle_count, 4);
}
#[test]
fn test_repeated_session_obtain_reuses_same_logical_rows() {
install_test_namespace();
let tmp_dir = tempfile::tempdir().unwrap();
let handle: ClientStorageThreadHandle =
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
let origin = ServoUrl::parse("https://example.com").unwrap().origin();
let webview = WebViewId::new(servo_base::id::TEST_PAINTER_ID);
let browsing_context = Into::<BrowsingContextId>::into(webview).to_string();
let first = obtain_bottle_map(
&handle,
StorageType::Session,
webview,
StorageIdentifier::SessionStorage,
origin.clone(),
);
let second = obtain_bottle_map(
&handle,
StorageType::Session,
webview,
StorageIdentifier::SessionStorage,
origin.clone(),
);
assert_eq!(first.bottle_id, second.bottle_id);
let registry = open_registry(&tmp_dir);
let session_shed_count: i64 = registry
.query_row(
"SELECT COUNT(*) FROM sheds WHERE storage_type = 'session' AND browsing_context = ?1;",
[browsing_context],
|row| row.get(0),
)
.unwrap();
let shelf_count: i64 = registry
.query_row(
"SELECT COUNT(*) FROM shelves WHERE origin = ?1;",
[origin.ascii_serialization()],
|row| row.get(0),
)
.unwrap();
let bucket_count: i64 = registry
.query_row("SELECT COUNT(*) FROM buckets;", [], |row| row.get(0))
.unwrap();
let bottle_count: i64 = registry
.query_row("SELECT COUNT(*) FROM bottles;", [], |row| row.get(0))
.unwrap();
assert_eq!(session_shed_count, 1);
assert_eq!(shelf_count, 1);
assert_eq!(bucket_count, 1);
assert_eq!(bottle_count, 1);
}
#[test]
fn test_local_persistence_and_estimate() {
install_test_namespace();
let tmp_dir = tempfile::tempdir().unwrap();
let handle: ClientStorageThreadHandle =
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
let origin = ServoUrl::parse("https://example.com").unwrap().origin();
let webview = WebViewId::new(servo_base::id::TEST_PAINTER_ID);
let storage_proxy_map = obtain_bottle_map(
&handle,
StorageType::Local,
webview,
StorageIdentifier::IndexedDB,
origin.clone(),
);
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persisted(origin.clone(), cb).unwrap();
assert!(!rx.recv().unwrap().unwrap());
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persist(origin.clone(), false, cb).unwrap();
assert!(!rx.recv().unwrap().unwrap());
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persisted(origin.clone(), cb).unwrap();
assert!(!rx.recv().unwrap().unwrap());
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persist(origin.clone(), true, cb).unwrap();
assert!(rx.recv().unwrap().unwrap());
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persisted(origin.clone(), cb).unwrap();
assert!(rx.recv().unwrap().unwrap());
let path = handle
.create_database(storage_proxy_map.bottle_id, "estimate".to_string())
.recv()
.unwrap()
.unwrap();
let payload = vec![0x5a; 8192];
std::fs::write(path.join("payload.bin"), &payload).unwrap();
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.estimate(origin, cb).unwrap();
let (usage, quota) = rx.recv().unwrap().unwrap();
assert!(usage >= payload.len() as u64);
assert!(quota > usage);
}
#[test]
fn test_storage_manager_operations_fail_for_opaque_origins() {
install_test_namespace();
let tmp_dir = tempfile::tempdir().unwrap();
let handle: ClientStorageThreadHandle =
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
let origin = ServoUrl::parse("data:text/plain,hello").unwrap().origin();
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persisted(origin.clone(), cb).unwrap();
assert!(rx.recv().unwrap().is_err());
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.persist(origin.clone(), true, cb).unwrap();
assert!(rx.recv().unwrap().is_err());
let (cb, rx) = GenericCallback::new_blocking().unwrap();
handle.estimate(origin, cb).unwrap();
assert!(rx.recv().unwrap().is_err());
}