in components/places/src/bookmark_sync/engine.rs [3343:3595]
fn test_apply_complex_bookmark_keywords() -> Result<()> {
use crate::storage::bookmarks::bookmarks_get_url_for_keyword;
// We don't provide an API for setting keywords locally, but we'll
// still round-trip and fix up keywords on the server.
let api = new_mem_api();
let writer = api.open_connection(ConnectionType::ReadWrite)?;
// Let's add some remote bookmarks with keywords.
let remote_records = json!([{
// A1 and A2 have the same URL and keyword, so we shouldn't
// reupload them.
"id": "bookmarkAAA1",
"type": "bookmark",
"parentid": "unfiled",
"parentName": "Unfiled",
"title": "A1",
"bmkUri": "http://example.com/a",
"keyword": "one",
}, {
"id": "bookmarkAAA2",
"type": "bookmark",
"parentid": "menu",
"parentName": "Menu",
"title": "A2",
"bmkUri": "http://example.com/a",
"keyword": "one",
}, {
// B1 and B2 have mismatched keywords, and we should reupload
// both of them. It's not specified which keyword wins, but
// reuploading both means we make them consistent.
"id": "bookmarkBBB1",
"type": "bookmark",
"parentid": "unfiled",
"parentName": "Unfiled",
"title": "B1",
"bmkUri": "http://example.com/b",
"keyword": "two",
}, {
"id": "bookmarkBBB2",
"type": "bookmark",
"parentid": "menu",
"parentName": "Menu",
"title": "B2",
"bmkUri": "http://example.com/b",
"keyword": "three",
}, {
// C1 has a keyword; C2 doesn't. As with B, which one wins
// depends on which record we apply last, and how SQLite
// processes the rows, but we should reupload both.
"id": "bookmarkCCC1",
"type": "bookmark",
"parentid": "unfiled",
"parentName": "Unfiled",
"title": "C1",
"bmkUri": "http://example.com/c",
"keyword": "four",
}, {
"id": "bookmarkCCC2",
"type": "bookmark",
"parentid": "menu",
"parentName": "Menu",
"title": "C2",
"bmkUri": "http://example.com/c",
}, {
// D has a keyword that needs to be cleaned up before
// inserting. In this case, we intentionally don't reupload.
"id": "bookmarkDDDD",
"type": "bookmark",
"parentid": "unfiled",
"parentName": "Unfiled",
"title": "D",
"bmkUri": "http://example.com/d",
"keyword": " FIVE ",
}, {
"id": "unfiled",
"type": "folder",
"parentid": "places",
"title": "Unfiled",
"children": ["bookmarkAAA1", "bookmarkBBB1", "bookmarkCCC1", "bookmarkDDDD"],
}, {
"id": "menu",
"type": "folder",
"parentid": "places",
"title": "Menu",
"children": ["bookmarkAAA2", "bookmarkBBB2", "bookmarkCCC2"],
}]);
let engine = create_sync_engine(&api);
let incoming = if let Value::Array(records) = remote_records {
records
.into_iter()
.map(IncomingBso::from_test_content)
.collect()
} else {
unreachable!("JSON records must be an array");
};
let mut outgoing = engine_apply_incoming(&engine, incoming);
outgoing.sort_by(|a, b| a.envelope.id.cmp(&b.envelope.id));
assert_local_json_tree(
&writer,
&BookmarkRootGuid::Root.as_guid(),
json!({
"guid": &BookmarkRootGuid::Root.as_guid(),
"children": [{
"guid": &BookmarkRootGuid::Menu.as_guid(),
"children": [{
"guid": "bookmarkAAA2",
"title": "A2",
"url": "http://example.com/a",
}, {
"guid": "bookmarkBBB2",
"title": "B2",
"url": "http://example.com/b",
}, {
"guid": "bookmarkCCC2",
"title": "C2",
"url": "http://example.com/c",
}],
}, {
"guid": &BookmarkRootGuid::Toolbar.as_guid(),
"children": [],
}, {
"guid": &BookmarkRootGuid::Unfiled.as_guid(),
"children": [{
"guid": "bookmarkAAA1",
"title": "A1",
"url": "http://example.com/a",
}, {
"guid": "bookmarkBBB1",
"title": "B1",
"url": "http://example.com/b",
}, {
"guid": "bookmarkCCC1",
"title": "C1",
"url": "http://example.com/c",
}, {
"guid": "bookmarkDDDD",
"title": "D",
"url": "http://example.com/d",
}],
}, {
"guid": &BookmarkRootGuid::Mobile.as_guid(),
"children": [],
}],
}),
);
// And verify our local keywords are correct, too.
let url_for_one = bookmarks_get_url_for_keyword(&writer, "one")?
.expect("Should have URL for keyword `one`");
assert_eq!(url_for_one.as_str(), "http://example.com/a");
let keyword_for_b = match (
bookmarks_get_url_for_keyword(&writer, "two")?,
bookmarks_get_url_for_keyword(&writer, "three")?,
) {
(Some(url), None) => {
assert_eq!(url.as_str(), "http://example.com/b");
"two".to_string()
}
(None, Some(url)) => {
assert_eq!(url.as_str(), "http://example.com/b");
"three".to_string()
}
(Some(_), Some(_)) => panic!("Should pick `two` or `three`, not both"),
(None, None) => panic!("Should have URL for either `two` or `three`"),
};
let keyword_for_c = match bookmarks_get_url_for_keyword(&writer, "four")? {
Some(url) => {
assert_eq!(url.as_str(), "http://example.com/c");
Some("four".to_string())
}
None => None,
};
let url_for_five = bookmarks_get_url_for_keyword(&writer, "five")?
.expect("Should have URL for keyword `five`");
assert_eq!(url_for_five.as_str(), "http://example.com/d");
let expected_outgoing_keywords = &[
("bookmarkBBB1", Some(keyword_for_b.clone())),
("bookmarkBBB2", Some(keyword_for_b.clone())),
("bookmarkCCC1", keyword_for_c.clone()),
("bookmarkCCC2", keyword_for_c.clone()),
("menu", None), // Roots always get uploaded on the first sync.
("mobile", None),
("toolbar", None),
("unfiled", None),
];
assert_eq!(
outgoing
.iter()
.map(|p| (
p.envelope.id.as_str(),
p.to_test_incoming_t::<BookmarkRecord>().keyword
))
.collect::<Vec<_>>(),
expected_outgoing_keywords,
"Should upload new bookmarks and fix up keywords",
);
// Now push the records back to the engine, so we can check what we're
// uploading.
engine
.set_uploaded(
ServerTimestamp(0),
expected_outgoing_keywords
.iter()
.map(|(id, _)| SyncGuid::from(id))
.collect(),
)
.expect("Should push synced changes back to the engine");
engine.sync_finished().expect("should work");
let mut synced_item_for_b = SyncedBookmarkItem::new();
synced_item_for_b
.validity(SyncedBookmarkValidity::Valid)
.kind(SyncedBookmarkKind::Bookmark)
.url(Some("http://example.com/b"))
.keyword(Some(&keyword_for_b));
let mut synced_item_for_c = SyncedBookmarkItem::new();
synced_item_for_c
.validity(SyncedBookmarkValidity::Valid)
.kind(SyncedBookmarkKind::Bookmark)
.url(Some("http://example.com/c"))
.keyword(Some(keyword_for_c.unwrap().as_str()));
let expected_synced_items = &[
ExpectedSyncedItem::with_properties("bookmarkBBB1", &synced_item_for_b, |a| {
a.parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
.title(Some("B1"))
}),
ExpectedSyncedItem::with_properties("bookmarkBBB2", &synced_item_for_b, |a| {
a.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
.title(Some("B2"))
}),
ExpectedSyncedItem::with_properties("bookmarkCCC1", &synced_item_for_c, |a| {
a.parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
.title(Some("C1"))
}),
ExpectedSyncedItem::with_properties("bookmarkCCC2", &synced_item_for_c, |a| {
a.parent_guid(Some(&BookmarkRootGuid::Menu.as_guid()))
.title(Some("C2"))
}),
];
for item in expected_synced_items {
item.check(&writer)?;
}
Ok(())
}