1use super::record::{
6 BookmarkItemRecord, BookmarkRecord, BookmarkRecordId, FolderRecord, LivemarkRecord,
7 QueryRecord, SeparatorRecord,
8};
9use super::{SyncedBookmarkKind, SyncedBookmarkValidity};
10use crate::error::*;
11use crate::storage::{
12 bookmarks::maybe_truncate_title,
13 tags::{validate_tag, ValidatedTag},
14 URL_LENGTH_MAX,
15};
16use crate::types::serialize_unknown_fields;
17use rusqlite::Connection;
18use serde_json::Value as JsonValue;
19use sql_support::{self, ConnExt};
20use std::{collections::HashSet, iter};
21use sync15::bso::{IncomingBso, IncomingKind};
22use sync15::ServerTimestamp;
23use sync_guid::Guid as SyncGuid;
24use url::Url;
25
26const RESULTS_AS_TAG_CONTENTS: &str = "7";
29
30pub struct IncomingApplicator<'a> {
33 db: &'a Connection,
34 default_max_variable_number: Option<usize>,
36}
37
38impl<'a> IncomingApplicator<'a> {
39 pub fn new(db: &'a Connection) -> Self {
40 Self {
41 db,
42 default_max_variable_number: None,
43 }
44 }
45
46 pub fn apply_bso(&self, record: IncomingBso) -> Result<()> {
47 let timestamp = record.envelope.modified;
48 let mut validity = SyncedBookmarkValidity::Valid;
49 let json_content = record.into_content_with_fixup::<BookmarkItemRecord>(|json| {
50 validity = fixup_bookmark_json(json)
51 });
52 match json_content.kind {
53 IncomingKind::Tombstone => {
54 self.store_incoming_tombstone(
55 timestamp,
56 BookmarkRecordId::from_payload_id(json_content.envelope.id.clone()).as_guid(),
57 )?;
58 }
59 IncomingKind::Content(item) => match item {
60 BookmarkItemRecord::Bookmark(b) => {
61 self.store_incoming_bookmark(timestamp, &b, validity)?
62 }
63 BookmarkItemRecord::Query(q) => {
64 self.store_incoming_query(timestamp, &q, validity)?
65 }
66 BookmarkItemRecord::Folder(f) => {
67 self.store_incoming_folder(timestamp, &f, validity)?
68 }
69 BookmarkItemRecord::Livemark(l) => {
70 self.store_incoming_livemark(timestamp, &l, validity)?
71 }
72 BookmarkItemRecord::Separator(s) => {
73 self.store_incoming_sep(timestamp, &s, validity)?
74 }
75 },
76 IncomingKind::Malformed => {
77 trace!(
78 "skipping malformed bookmark record: {}",
79 json_content.envelope.id
80 );
81 error_support::report_error!(
82 "malformed-incoming-bookmark",
83 "Malformed bookmark record"
84 );
85 }
86 }
87 Ok(())
88 }
89
90 fn store_incoming_bookmark(
91 &self,
92 modified: ServerTimestamp,
93 b: &BookmarkRecord,
94 mut validity: SyncedBookmarkValidity,
95 ) -> Result<()> {
96 let url = match self.maybe_store_href(b.url.as_deref()) {
97 Ok(u) => Some(String::from(u)),
98 Err(e) => {
99 warn!("Incoming bookmark has an invalid URL: {:?}", e);
100 set_replace(&mut validity);
102 None
103 }
104 };
105
106 self.db.execute_cached(
107 r#"REPLACE INTO moz_bookmarks_synced(guid, parentGuid, serverModified, needsMerge, kind,
108 dateAdded, title, keyword, validity, unknownFields, placeId)
109 VALUES(:guid, :parentGuid, :serverModified, 1, :kind,
110 :dateAdded, NULLIF(:title, ""), :keyword, :validity, :unknownFields,
111 CASE WHEN :url ISNULL
112 THEN NULL
113 ELSE (SELECT id FROM moz_places
114 WHERE url_hash = hash(:url) AND
115 url = :url)
116 END
117 )"#,
118 &[
119 (
120 ":guid",
121 &b.record_id.as_guid().as_str() as &dyn rusqlite::ToSql,
122 ),
123 (
124 ":parentGuid",
125 &b.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
126 ),
127 (":serverModified", &modified.as_millis()),
128 (":kind", &SyncedBookmarkKind::Bookmark),
129 (":dateAdded", &b.date_added),
130 (":title", &maybe_truncate_title(&b.title.as_deref())),
131 (":keyword", &b.keyword),
132 (":validity", &validity),
133 (":url", &url),
134 (":unknownFields", &serialize_unknown_fields(&b.unknown_fields)?),
135 ],
136 )?;
137 for t in b.tags.iter() {
138 self.db.execute_cached(
139 "INSERT OR IGNORE INTO moz_tags(tag, lastModified)
140 VALUES(:tag, now())",
141 &[(":tag", &t)],
142 )?;
143 self.db.execute_cached(
144 "INSERT INTO moz_bookmarks_synced_tag_relation(itemId, tagId)
145 VALUES((SELECT id FROM moz_bookmarks_synced
146 WHERE guid = :guid),
147 (SELECT id FROM moz_tags
148 WHERE tag = :tag))",
149 &[(":guid", b.record_id.as_guid().as_str()), (":tag", t)],
150 )?;
151 }
152 Ok(())
153 }
154
155 fn store_incoming_folder(
156 &self,
157 modified: ServerTimestamp,
158 f: &FolderRecord,
159 validity: SyncedBookmarkValidity,
160 ) -> Result<()> {
161 self.db.execute_cached(
162 r#"REPLACE INTO moz_bookmarks_synced(guid, parentGuid, serverModified, needsMerge, kind,
163 dateAdded, validity, unknownFields, title)
164 VALUES(:guid, :parentGuid, :serverModified, 1, :kind,
165 :dateAdded, :validity, :unknownFields, NULLIF(:title, ""))"#,
166 &[
167 (
168 ":guid",
169 &f.record_id.as_guid().as_str() as &dyn rusqlite::ToSql,
170 ),
171 (
172 ":parentGuid",
173 &f.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
174 ),
175 (":serverModified", &modified.as_millis()),
176 (":kind", &SyncedBookmarkKind::Folder),
177 (":dateAdded", &f.date_added),
178 (":title", &maybe_truncate_title(&f.title.as_deref())),
179 (":validity", &validity),
180 (
181 ":unknownFields",
182 &serialize_unknown_fields(&f.unknown_fields)?,
183 ),
184 ],
185 )?;
186 let default_max_variable_number = self
187 .default_max_variable_number
188 .unwrap_or_else(sql_support::default_max_variable_number);
189 sql_support::each_sized_chunk(
190 &f.children,
191 default_max_variable_number - 1,
194 |chunk, offset| -> Result<()> {
195 let sql = format!(
196 "INSERT INTO moz_bookmarks_synced_structure(guid, parentGuid, position)
197 VALUES {}",
198 sql_support::repeat_display(chunk.len(), ",", |index, f| {
205 let position = offset + index;
209 write!(f, "(?{}, ?1, {})", index + 2, position)
210 })
211 );
212 self.db.execute(
213 &sql,
214 rusqlite::params_from_iter(
215 iter::once(&f.record_id)
216 .chain(chunk.iter())
217 .map(|id| id.as_guid().as_str()),
218 ),
219 )?;
220 Ok(())
221 },
222 )?;
223 Ok(())
224 }
225
226 fn store_incoming_tombstone(&self, modified: ServerTimestamp, guid: &SyncGuid) -> Result<()> {
227 self.db.execute_cached(
228 "REPLACE INTO moz_bookmarks_synced(guid, parentGuid, serverModified, needsMerge,
229 dateAdded, isDeleted)
230 VALUES(:guid, NULL, :serverModified, 1, 0, 1)",
231 &[
232 (":guid", guid as &dyn rusqlite::ToSql),
233 (":serverModified", &modified.as_millis()),
234 ],
235 )?;
236 Ok(())
237 }
238
239 fn maybe_rewrite_and_store_query_url(
240 &self,
241 tag_folder_name: Option<&str>,
242 record_id: &BookmarkRecordId,
243 url: Url,
244 validity: &mut SyncedBookmarkValidity,
245 ) -> Result<Option<Url>> {
246 let maybe_url = {
249 let parse = url::form_urlencoded::parse(url.path().as_bytes());
255 if parse
256 .clone()
257 .any(|(k, v)| k == "type" && v == RESULTS_AS_TAG_CONTENTS)
258 {
259 if let Some(t) = tag_folder_name {
260 validate_tag(t)
261 .ensure_valid()
262 .and_then(|tag| Ok(Url::parse(&format!("place:tag={}", tag))?))
263 .map(|url| {
264 set_reupload(validity);
265 Some(url)
266 })
267 .unwrap_or_else(|_| {
268 set_replace(validity);
269 None
270 })
271 } else {
272 set_replace(validity);
273 None
274 }
275 } else {
276 if parse.clone().any(|(k, _)| k == "folder") {
280 if parse.clone().any(|(k, v)| k == "excludeItems" && v == "1") {
281 Some(url)
282 } else {
283 let tail = url::form_urlencoded::Serializer::new(String::new())
286 .extend_pairs(parse)
287 .append_pair("excludeItems", "1")
288 .finish();
289 set_reupload(validity);
290 Some(Url::parse(&format!("place:{}", tail))?)
291 }
292 } else {
293 Some(url)
295 }
296 }
297 };
298 Ok(match self.maybe_store_url(maybe_url) {
299 Ok(url) => Some(url),
300 Err(e) => {
301 warn!("query {} has invalid URL: {:?}", record_id.as_guid(), e);
302 set_replace(validity);
303 None
304 }
305 })
306 }
307
308 fn store_incoming_query(
309 &self,
310 modified: ServerTimestamp,
311 q: &QueryRecord,
312 mut validity: SyncedBookmarkValidity,
313 ) -> Result<()> {
314 let url = match q.url.as_ref().and_then(|href| Url::parse(href).ok()) {
315 Some(url) => self.maybe_rewrite_and_store_query_url(
316 q.tag_folder_name.as_deref(),
317 &q.record_id,
318 url,
319 &mut validity,
320 )?,
321 None => {
322 warn!("query {} has invalid URL", &q.record_id.as_guid(),);
323 set_replace(&mut validity);
324 None
325 }
326 };
327
328 self.db.execute_cached(
329 r#"REPLACE INTO moz_bookmarks_synced(guid, parentGuid, serverModified, needsMerge, kind,
330 dateAdded, title, validity, unknownFields, placeId)
331 VALUES(:guid, :parentGuid, :serverModified, 1, :kind,
332 :dateAdded, NULLIF(:title, ""), :validity, :unknownFields,
333 (SELECT id FROM moz_places
334 WHERE url_hash = hash(:url) AND
335 url = :url
336 )
337 )"#,
338 &[
339 (
340 ":guid",
341 &q.record_id.as_guid().as_str() as &dyn rusqlite::ToSql,
342 ),
343 (
344 ":parentGuid",
345 &q.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
346 ),
347 (":serverModified", &modified.as_millis()),
348 (":kind", &SyncedBookmarkKind::Query),
349 (":dateAdded", &q.date_added),
350 (":title", &maybe_truncate_title(&q.title.as_deref())),
351 (":validity", &validity),
352 (
353 ":unknownFields",
354 &serialize_unknown_fields(&q.unknown_fields)?,
355 ),
356 (":url", &url.map(String::from)),
357 ],
358 )?;
359 Ok(())
360 }
361
362 fn store_incoming_livemark(
363 &self,
364 modified: ServerTimestamp,
365 l: &LivemarkRecord,
366 mut validity: SyncedBookmarkValidity,
367 ) -> Result<()> {
368 fn validate_href(h: &Option<String>, guid: &SyncGuid, what: &str) -> Option<String> {
370 match h {
371 Some(h) => match Url::parse(h) {
372 Ok(url) => {
373 let s = url.to_string();
374 if s.len() > URL_LENGTH_MAX {
375 warn!("Livemark {} has a {} URL which is too long", &guid, what);
376 None
377 } else {
378 Some(s)
379 }
380 }
381 Err(e) => {
382 warn!("Livemark {} has an invalid {} URL: {:?}", &guid, what, e);
383 None
384 }
385 },
386 None => {
387 warn!("Livemark {} has no {} URL", &guid, what);
388 None
389 }
390 }
391 }
392 let feed_url = validate_href(&l.feed_url, l.record_id.as_guid(), "feed");
393 let site_url = validate_href(&l.site_url, l.record_id.as_guid(), "site");
394
395 if feed_url.is_none() {
396 set_replace(&mut validity);
397 }
398
399 self.db.execute_cached(
400 "REPLACE INTO moz_bookmarks_synced(guid, parentGuid, serverModified, needsMerge, kind,
401 dateAdded, title, feedURL, siteURL, validity)
402 VALUES(:guid, :parentGuid, :serverModified, 1, :kind,
403 :dateAdded, :title, :feedUrl, :siteUrl, :validity)",
404 &[
405 (
406 ":guid",
407 &l.record_id.as_guid().as_str() as &dyn rusqlite::ToSql,
408 ),
409 (
410 ":parentGuid",
411 &l.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
412 ),
413 (":serverModified", &modified.as_millis()),
414 (":kind", &SyncedBookmarkKind::Livemark),
415 (":dateAdded", &l.date_added),
416 (":title", &l.title),
417 (":feedUrl", &feed_url),
418 (":siteUrl", &site_url),
419 (":validity", &validity),
420 ],
421 )?;
422 Ok(())
423 }
424
425 fn store_incoming_sep(
426 &self,
427 modified: ServerTimestamp,
428 s: &SeparatorRecord,
429 validity: SyncedBookmarkValidity,
430 ) -> Result<()> {
431 self.db.execute_cached(
432 "REPLACE INTO moz_bookmarks_synced(guid, parentGuid, serverModified, needsMerge, kind,
433 dateAdded, validity, unknownFields)
434 VALUES(:guid, :parentGuid, :serverModified, 1, :kind,
435 :dateAdded, :validity, :unknownFields)",
436 &[
437 (
438 ":guid",
439 &s.record_id.as_guid().as_str() as &dyn rusqlite::ToSql,
440 ),
441 (
442 ":parentGuid",
443 &s.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
444 ),
445 (":serverModified", &modified.as_millis()),
446 (":kind", &SyncedBookmarkKind::Separator),
447 (":dateAdded", &s.date_added),
448 (":validity", &validity),
449 (
450 ":unknownFields",
451 &serialize_unknown_fields(&s.unknown_fields)?,
452 ),
453 ],
454 )?;
455 Ok(())
456 }
457
458 fn maybe_store_href(&self, href: Option<&str>) -> Result<Url> {
459 if let Some(href) = href {
460 self.maybe_store_url(Some(Url::parse(href)?))
461 } else {
462 self.maybe_store_url(None)
463 }
464 }
465
466 fn maybe_store_url(&self, url: Option<Url>) -> Result<Url> {
467 if let Some(url) = url {
468 if url.as_str().len() > URL_LENGTH_MAX {
469 return Err(Error::InvalidPlaceInfo(InvalidPlaceInfo::UrlTooLong));
470 }
471 self.db.execute_cached(
472 "INSERT OR IGNORE INTO moz_places(guid, url, url_hash, frecency)
473 VALUES(IFNULL((SELECT guid FROM moz_places
474 WHERE url_hash = hash(:url) AND
475 url = :url),
476 generate_guid()), :url, hash(:url),
477 (CASE substr(:url, 1, 6) WHEN 'place:' THEN 0 ELSE -1 END))",
478 &[(":url", &url.as_str())],
479 )?;
480 Ok(url)
481 } else {
482 Err(Error::InvalidPlaceInfo(InvalidPlaceInfo::NoUrl))
483 }
484 }
485}
486
487fn fixup_bookmark_json(json: &mut JsonValue) -> SyncedBookmarkValidity {
493 let mut validity = SyncedBookmarkValidity::Valid;
494 if let JsonValue::Object(ref mut obj) = json {
497 obj.entry("parentid")
498 .and_modify(|v| fixup_optional_str(v, &mut validity));
499 obj.entry("title")
500 .and_modify(|v| fixup_optional_str(v, &mut validity));
501 obj.entry("bmkUri")
502 .and_modify(|v| fixup_optional_str(v, &mut validity));
503 obj.entry("folderName")
504 .and_modify(|v| fixup_optional_str(v, &mut validity));
505 obj.entry("feedUri")
506 .and_modify(|v| fixup_optional_str(v, &mut validity));
507 obj.entry("siteUri")
508 .and_modify(|v| fixup_optional_str(v, &mut validity));
509 obj.entry("dateAdded")
510 .and_modify(|v| fixup_optional_i64(v, &mut validity));
511 obj.entry("keyword")
512 .and_modify(|v| fixup_optional_keyword(v, &mut validity));
513 obj.entry("tags")
514 .and_modify(|v| fixup_optional_tags(v, &mut validity));
515 }
516 validity
517}
518
519fn fixup_optional_str(val: &mut JsonValue, validity: &mut SyncedBookmarkValidity) {
520 if !matches!(val, JsonValue::String(_) | JsonValue::Null) {
521 set_reupload(validity);
522 *val = JsonValue::Null;
523 }
524}
525
526fn fixup_optional_i64(val: &mut JsonValue, validity: &mut SyncedBookmarkValidity) {
527 match val {
528 JsonValue::Number(_) => (),
530 JsonValue::String(s) => {
531 set_reupload(validity);
532 if let Ok(n) = s.parse::<u64>() {
533 *val = JsonValue::Number(n.into());
534 } else {
535 *val = JsonValue::Null;
536 }
537 }
538 JsonValue::Null => (),
539 _ => {
540 set_reupload(validity);
541 *val = JsonValue::Null;
542 }
543 }
544}
545
546fn fixup_optional_keyword(val: &mut JsonValue, validity: &mut SyncedBookmarkValidity) {
551 match val {
552 JsonValue::String(s) => {
553 let fixed = s.trim().to_lowercase();
554 if fixed.is_empty() {
555 *val = JsonValue::Null;
556 } else if fixed != *s {
557 *val = JsonValue::String(fixed);
558 }
559 }
560 JsonValue::Null => (),
561 _ => {
562 set_reupload(validity);
563 *val = JsonValue::Null;
564 }
565 }
566}
567
568fn fixup_optional_tags(val: &mut JsonValue, validity: &mut SyncedBookmarkValidity) {
569 match val {
570 JsonValue::Array(ref tags) => {
571 let mut valid_tags = HashSet::with_capacity(tags.len());
572 for v in tags {
573 if let JsonValue::String(tag) = v {
574 let tag = match validate_tag(tag) {
575 ValidatedTag::Invalid(t) => {
576 trace!("Incoming bookmark has invalid tag: {:?}", t);
577 set_reupload(validity);
578 continue;
579 }
580 ValidatedTag::Normalized(t) => {
581 set_reupload(validity);
582 t
583 }
584 ValidatedTag::Original(t) => t,
585 };
586 if !valid_tags.insert(tag) {
587 trace!("Incoming bookmark has duplicate tag: {:?}", tag);
588 set_reupload(validity);
589 }
590 } else {
591 trace!("Incoming bookmark has unexpected tag: {:?}", v);
592 set_reupload(validity);
593 }
594 }
595 *val = JsonValue::Array(valid_tags.into_iter().map(JsonValue::from).collect());
596 }
597 JsonValue::Null => (),
598 _ => {
599 set_reupload(validity);
600 *val = JsonValue::Null;
601 }
602 }
603}
604
605fn set_replace(validity: &mut SyncedBookmarkValidity) {
606 if *validity < SyncedBookmarkValidity::Replace {
607 *validity = SyncedBookmarkValidity::Replace;
608 }
609}
610
611fn set_reupload(validity: &mut SyncedBookmarkValidity) {
612 if *validity < SyncedBookmarkValidity::Reupload {
613 *validity = SyncedBookmarkValidity::Reupload;
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use crate::api::places_api::{test::new_mem_api, PlacesApi};
621 use crate::bookmark_sync::record::{BookmarkItemRecord, FolderRecord};
622 use crate::bookmark_sync::tests::SyncedBookmarkItem;
623 use crate::storage::bookmarks::BookmarkRootGuid;
624 use crate::types::UnknownFields;
625 use serde_json::{json, Value};
626
627 fn apply_incoming(api: &PlacesApi, records_json: Value) {
628 let db = api.get_sync_connection().expect("should get a db mutex");
629 let conn = db.lock();
630
631 let mut applicator = IncomingApplicator::new(&conn);
632 applicator.default_max_variable_number = Some(5);
633
634 match records_json {
635 Value::Array(records) => {
636 for record in records {
637 applicator
638 .apply_bso(IncomingBso::from_test_content(record))
639 .expect("Should apply incoming and stage outgoing records");
640 }
641 }
642 Value::Object(_) => {
643 applicator
644 .apply_bso(IncomingBso::from_test_content(records_json))
645 .expect("Should apply incoming and stage outgoing records");
646 }
647 _ => panic!("unexpected json value"),
648 }
649 }
650
651 fn assert_incoming_creates_mirror_item(record_json: Value, expected: &SyncedBookmarkItem) {
652 let guid = record_json["id"]
653 .as_str()
654 .expect("id must be a string")
655 .to_string();
656 let api = new_mem_api();
657 apply_incoming(&api, record_json);
658 let got = SyncedBookmarkItem::get(&api.get_sync_connection().unwrap().lock(), &guid.into())
659 .expect("should work")
660 .expect("item should exist");
661 assert_eq!(*expected, got);
662 }
663
664 #[test]
665 fn test_apply_bookmark() {
666 assert_incoming_creates_mirror_item(
667 json!({
668 "id": "bookmarkAAAA",
669 "type": "bookmark",
670 "parentid": "unfiled",
671 "parentName": "unfiled",
672 "dateAdded": 1_381_542_355_843u64,
673 "title": "A",
674 "bmkUri": "http://example.com/a",
675 "tags": ["foo", "bar"],
676 "keyword": "baz",
677 }),
678 SyncedBookmarkItem::new()
679 .validity(SyncedBookmarkValidity::Valid)
680 .kind(SyncedBookmarkKind::Bookmark)
681 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
682 .title(Some("A"))
683 .url(Some("http://example.com/a"))
684 .tags(vec!["foo".into(), "bar".into()])
685 .keyword(Some("baz")),
686 );
687 }
688
689 #[test]
690 fn test_apply_folder() {
691 let children = (1..6)
694 .map(|i| SyncGuid::from(format!("{:A>12}", i)))
695 .collect::<Vec<_>>();
696 let value = serde_json::to_value(BookmarkItemRecord::from(FolderRecord {
697 record_id: BookmarkRecordId::from_payload_id("folderAAAAAA".into()),
698 parent_record_id: Some(BookmarkRecordId::from_payload_id("unfiled".into())),
699 parent_title: Some("unfiled".into()),
700 date_added: Some(0),
701 has_dupe: true,
702 title: Some("A".into()),
703 children: children
704 .iter()
705 .map(|guid| BookmarkRecordId::from(guid.clone()))
706 .collect(),
707 unknown_fields: UnknownFields::new(),
708 }))
709 .expect("Should serialize folder with children");
710 assert_incoming_creates_mirror_item(
711 value,
712 SyncedBookmarkItem::new()
713 .validity(SyncedBookmarkValidity::Valid)
714 .kind(SyncedBookmarkKind::Folder)
715 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
716 .title(Some("A"))
717 .children(children),
718 );
719 }
720
721 #[test]
722 fn test_apply_tombstone() {
723 assert_incoming_creates_mirror_item(
724 json!({
725 "id": "deadbeef____",
726 "deleted": true
727 }),
728 SyncedBookmarkItem::new()
729 .validity(SyncedBookmarkValidity::Valid)
730 .deleted(true),
731 );
732 }
733
734 #[test]
735 fn test_apply_query() {
736 assert_incoming_creates_mirror_item(
741 json!({
742 "id": "query1______",
743 "type": "query",
744 "parentid": "unfiled",
745 "parentName": "Unfiled Bookmarks",
746 "dateAdded": 1_381_542_355_843u64,
747 "title": "Some query",
748 "bmkUri": "place:tag=foo",
749 }),
750 SyncedBookmarkItem::new()
751 .validity(SyncedBookmarkValidity::Valid)
752 .kind(SyncedBookmarkKind::Query)
753 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
754 .title(Some("Some query"))
755 .url(Some("place:tag=foo")),
756 );
757
758 assert_incoming_creates_mirror_item(
761 json!({
762 "id": "query1______",
763 "type": "query",
764 "parentid": "unfiled",
765 "bmkUri": "place:type=7",
766 "folderName": "a-folder-name",
767 }),
768 SyncedBookmarkItem::new()
769 .validity(SyncedBookmarkValidity::Reupload)
770 .kind(SyncedBookmarkKind::Query)
771 .url(Some("place:tag=a-folder-name")),
772 );
773
774 assert_incoming_creates_mirror_item(
777 json!({
778 "id": "query1______",
779 "type": "query",
780 "parentid": "unfiled",
781 "bmkUri": "place:type=7",
782 "folderName": "",
783 }),
784 SyncedBookmarkItem::new()
785 .validity(SyncedBookmarkValidity::Replace)
786 .kind(SyncedBookmarkKind::Query)
787 .url(None),
788 );
789
790 assert_incoming_creates_mirror_item(
793 json!({
794 "id": "query1______",
795 "type": "query",
796 "parentid": "unfiled",
797 "bmkUri": "place:folder=123",
798 }),
799 SyncedBookmarkItem::new()
800 .validity(SyncedBookmarkValidity::Reupload)
801 .kind(SyncedBookmarkKind::Query)
802 .url(Some("place:folder=123&excludeItems=1")),
803 );
804
805 assert_incoming_creates_mirror_item(
808 json!({
809 "id": "query1______",
810 "type": "query",
811 "parentid": "unfiled",
812 "bmkUri": "place:folder=123&excludeItems=1",
813 }),
814 SyncedBookmarkItem::new()
815 .validity(SyncedBookmarkValidity::Valid)
816 .kind(SyncedBookmarkKind::Query)
817 .url(Some("place:folder=123&excludeItems=1")),
818 );
819
820 assert_incoming_creates_mirror_item(
822 json!({
823 "id": "query1______",
824 "type": "query",
825 "parentid": "unfiled",
826 "bmkUri": "foo",
827 }),
828 SyncedBookmarkItem::new()
829 .validity(SyncedBookmarkValidity::Replace)
830 .kind(SyncedBookmarkKind::Query)
831 .url(None),
832 );
833
834 assert_incoming_creates_mirror_item(
836 json!({
837 "id": "query1______",
838 "type": "query",
839 "parentid": "unfiled",
840 }),
841 SyncedBookmarkItem::new()
842 .validity(SyncedBookmarkValidity::Replace)
843 .kind(SyncedBookmarkKind::Query)
844 .url(None),
845 );
846 }
847
848 #[test]
849 fn test_apply_sep() {
850 assert_incoming_creates_mirror_item(
852 json!({
853 "id": "sep1________",
854 "type": "separator",
855 "parentid": "unfiled",
856 "parentName": "Unfiled Bookmarks",
857 }),
858 SyncedBookmarkItem::new()
859 .validity(SyncedBookmarkValidity::Valid)
860 .kind(SyncedBookmarkKind::Separator)
861 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
862 .needs_merge(true),
863 );
864 }
865
866 #[test]
867 fn test_apply_livemark() {
868 assert_incoming_creates_mirror_item(
870 json!({
871 "id": "livemark1___",
872 "type": "livemark",
873 "parentid": "unfiled",
874 "parentName": "Unfiled Bookmarks",
875 }),
876 SyncedBookmarkItem::new()
877 .validity(SyncedBookmarkValidity::Replace)
878 .kind(SyncedBookmarkKind::Livemark)
879 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
880 .needs_merge(true)
881 .feed_url(None)
882 .site_url(None),
883 );
884 assert_incoming_creates_mirror_item(
887 json!({
888 "id": "livemark1___",
889 "type": "livemark",
890 "parentid": "unfiled",
891 "parentName": "Unfiled Bookmarks",
892 "feedUri": "http://example.com",
893 "siteUri": "foo"
894 }),
895 SyncedBookmarkItem::new()
896 .validity(SyncedBookmarkValidity::Valid)
897 .kind(SyncedBookmarkKind::Livemark)
898 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
899 .needs_merge(true)
900 .feed_url(Some("http://example.com/"))
901 .site_url(None),
902 );
903 assert_incoming_creates_mirror_item(
905 json!({
906 "id": "livemark1___",
907 "type": "livemark",
908 "parentid": "unfiled",
909 "parentName": "Unfiled Bookmarks",
910 "feedUri": "http://example.com",
911 "siteUri": "http://example.com/something"
912 }),
913 SyncedBookmarkItem::new()
914 .validity(SyncedBookmarkValidity::Valid)
915 .kind(SyncedBookmarkKind::Livemark)
916 .parent_guid(Some(&BookmarkRootGuid::Unfiled.as_guid()))
917 .needs_merge(true)
918 .feed_url(Some("http://example.com/"))
919 .site_url(Some("http://example.com/something")),
920 );
921 }
922
923 #[test]
924 fn test_apply_unknown() {
925 let api = new_mem_api();
926 let db = api.get_sync_connection().expect("should get a db mutex");
927 let conn = db.lock();
928 let applicator = IncomingApplicator::new(&conn);
929
930 let record = json!({
931 "id": "unknownAAAA",
932 "type": "fancy",
933 });
934 let inc = IncomingBso::from_test_content(record);
935 applicator
937 .apply_bso(inc)
938 .expect("Should not fail with a record with unknown type");
939 }
940}