1use super::super::bookmarks::json_tree::{self, FetchDepth};
6use super::*;
7use rusqlite::Row;
8
9fn noisy_debug_assert_eq<T: std::cmp::PartialEq + std::fmt::Debug>(a: &T, b: &T, msg: &str) {
11 debug_assert_eq!(a, b);
12 if a != b {
13 error_support::report_error!(
14 "places-bookmarks-corruption",
15 "check failed: {}: {:?} != {:?}",
16 msg,
17 a,
18 b
19 );
20 }
21}
22
23fn noisy_debug_assert(v: bool, msg: &str) {
24 debug_assert!(v);
25 if !v {
26 error_support::report_error!(
27 "places-bookmark-corruption",
28 "check failed: {}: expected true, got false",
29 msg
30 );
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct BookmarkData {
37 pub guid: SyncGuid,
38 pub parent_guid: SyncGuid,
39 pub position: u32,
40 pub date_added: Timestamp,
41 pub last_modified: Timestamp,
42 pub url: Url,
43 pub title: Option<String>,
44}
45
46impl From<BookmarkData> for Item {
47 fn from(b: BookmarkData) -> Self {
48 Item::Bookmark { b }
49 }
50}
51
52#[cfg(test)]
54impl PartialEq for BookmarkData {
55 fn eq(&self, other: &Self) -> bool {
56 self.guid == other.guid
57 && self.parent_guid == other.parent_guid
58 && self.position == other.position
59 && self.url == other.url
60 && self.title == other.title
61 }
62}
63
64#[derive(Debug, Clone)]
65pub struct Separator {
66 pub guid: SyncGuid,
67 pub date_added: Timestamp,
68 pub last_modified: Timestamp,
69 pub parent_guid: SyncGuid,
70 pub position: u32,
71}
72
73impl From<Separator> for Item {
74 fn from(s: Separator) -> Self {
75 Item::Separator { s }
76 }
77}
78
79#[derive(Debug, Clone, Default)]
80pub struct Folder {
81 pub guid: SyncGuid,
82 pub date_added: Timestamp,
83 pub last_modified: Timestamp,
84 pub parent_guid: Option<SyncGuid>, pub position: u32,
87 pub title: Option<String>,
88 pub child_guids: Option<Vec<SyncGuid>>,
91 pub child_nodes: Option<Vec<Item>>,
92}
93
94impl From<Folder> for Item {
95 fn from(f: Folder) -> Self {
96 Item::Folder { f }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub enum Item {
103 Bookmark { b: BookmarkData },
104 Separator { s: Separator },
105 Folder { f: Folder },
106}
107
108macro_rules! impl_common_bookmark_getter {
111 ($getter_name:ident, $T:ty) => {
112 pub fn $getter_name(&self) -> &$T {
113 match self {
114 Item::Bookmark { b } => &b.$getter_name,
115 Item::Separator { s } => &s.$getter_name,
116 Item::Folder { f } => &f.$getter_name,
117 }
118 }
119 };
120}
121
122impl Item {
123 impl_common_bookmark_getter!(guid, SyncGuid);
124 impl_common_bookmark_getter!(position, u32);
125 impl_common_bookmark_getter!(date_added, Timestamp);
126 impl_common_bookmark_getter!(last_modified, Timestamp);
127 pub fn parent_guid(&self) -> Option<&SyncGuid> {
128 match self {
129 Item::Bookmark { b } => Some(&b.parent_guid),
130 Item::Folder { f } => f.parent_guid.as_ref(),
131 Item::Separator { s } => Some(&s.parent_guid),
132 }
133 }
134}
135
136fn folder_from_node_with_parent_info(
141 f: json_tree::FolderNode,
142 parent_guid: Option<SyncGuid>,
143 position: u32,
144 depth_left: usize,
145) -> Folder {
146 let guid = f.guid.expect("all items have guids");
147 let child_guids = Some(
150 f.children
151 .iter()
152 .map(|child| child.guid().clone())
153 .collect(),
154 );
155 let child_nodes = if depth_left != 0 {
156 Some(
157 f.children
158 .into_iter()
159 .enumerate()
160 .map(|(child_pos, child)| {
161 item_from_node_with_parent_info(
162 child,
163 guid.clone(),
164 child_pos as u32,
165 depth_left - 1,
166 )
167 })
168 .collect(),
169 )
170 } else {
171 None
172 };
173 Folder {
174 guid,
175 parent_guid,
176 position,
177 child_nodes,
178 child_guids,
179 title: f.title,
180 date_added: f.date_added.expect("always get dates"),
181 last_modified: f.last_modified.expect("always get dates"),
182 }
183}
184
185fn item_from_node_with_parent_info(
186 n: json_tree::BookmarkTreeNode,
187 parent_guid: SyncGuid,
188 position: u32,
189 depth_left: usize,
190) -> Item {
191 match n {
192 json_tree::BookmarkTreeNode::Bookmark { b } => BookmarkData {
193 guid: b.guid.expect("all items have guids"),
194 parent_guid,
195 position,
196 url: b.url,
197 title: b.title,
198 date_added: b.date_added.expect("always get dates"),
199 last_modified: b.last_modified.expect("always get dates"),
200 }
201 .into(),
202 json_tree::BookmarkTreeNode::Separator { s } => Separator {
203 guid: s.guid.expect("all items have guids"),
204 parent_guid,
205 position,
206 date_added: s.date_added.expect("always get dates"),
207 last_modified: s.last_modified.expect("always get dates"),
208 }
209 .into(),
210 json_tree::BookmarkTreeNode::Folder { f } => {
211 folder_from_node_with_parent_info(f, Some(parent_guid), position, depth_left).into()
212 }
213 }
214}
215
216pub fn fetch_tree(db: &PlacesDb, item_guid: &SyncGuid) -> Result<Option<Item>> {
219 fetch_tree_with_depth(db, item_guid, &FetchDepth::Deepest)
220}
221
222pub fn fetch_tree_with_depth(
225 db: &PlacesDb,
226 item_guid: &SyncGuid,
227 target_depth: &FetchDepth,
228) -> Result<Option<Item>> {
229 let (tree, parent_guid, position) = if let Some((tree, parent_guid, position)) =
230 json_tree::fetch_tree(db, item_guid, target_depth)?
231 {
232 (tree, parent_guid, position)
233 } else {
234 return Ok(None);
235 };
236 Ok(Some(match tree {
238 json_tree::BookmarkTreeNode::Folder { f } => {
239 noisy_debug_assert(
240 parent_guid.is_none() ^ (f.guid.as_ref() != Some(BookmarkRootGuid::Root.guid())),
241 "only root has no parent",
242 );
243 let depth_left = match target_depth {
244 FetchDepth::Specific(v) => *v,
245 FetchDepth::Deepest => usize::MAX,
246 };
247 folder_from_node_with_parent_info(f, parent_guid, position, depth_left).into()
248 }
249 _ => item_from_node_with_parent_info(
250 tree,
251 parent_guid.expect("must have parent"),
252 position,
253 0,
254 ),
255 }))
256}
257
258pub fn fetch_bookmarks_by_url(db: &PlacesDb, url: &Url) -> Result<Vec<BookmarkData>> {
259 let nodes = crate::storage::bookmarks::get_raw_bookmarks_for_url(db, url)?
260 .into_iter()
261 .map(|rb| {
262 noisy_debug_assert_eq(&rb.child_count, &0, "child count should be zero");
265 noisy_debug_assert_eq(
266 &rb.bookmark_type,
267 &BookmarkType::Bookmark,
268 "not a bookmark!",
269 );
270 noisy_debug_assert(rb.url.as_ref() == Some(url), "urls don't match");
272 noisy_debug_assert(rb.parent_guid.is_some(), "no parent guid");
273 BookmarkData {
274 guid: rb.guid,
275 parent_guid: rb
276 .parent_guid
277 .unwrap_or_else(|| BookmarkRootGuid::Unfiled.into()),
278 position: rb.position,
279 date_added: rb.date_added,
280 last_modified: rb.date_modified,
281 url: url.clone(),
282 title: rb.title,
283 }
284 })
285 .collect::<Vec<_>>();
286 Ok(nodes)
287}
288
289pub fn fetch_bookmark(
295 db: &PlacesDb,
296 item_guid: &SyncGuid,
297 get_direct_children: bool,
298) -> Result<Option<Item>> {
299 let depth = if get_direct_children {
300 FetchDepth::Specific(1)
301 } else {
302 FetchDepth::Specific(0)
303 };
304 fetch_tree_with_depth(db, item_guid, &depth)
305}
306
307fn bookmark_from_row(row: &Row<'_>) -> Result<Option<BookmarkData>> {
308 Ok(
309 match row
310 .get::<_, Option<String>>("url")?
311 .and_then(|href| url::Url::parse(&href).ok())
312 {
313 Some(url) => Some(BookmarkData {
314 guid: row.get("guid")?,
315 parent_guid: row.get("parentGuid")?,
316 position: row.get("position")?,
317 date_added: row.get("dateAdded")?,
318 last_modified: row.get("lastModified")?,
319 title: row.get("title")?,
320 url,
321 }),
322 None => None,
323 },
324 )
325}
326
327pub fn search_bookmarks(db: &PlacesDb, search: &str, limit: u32) -> Result<Vec<BookmarkData>> {
328 let scope = db.begin_interrupt_scope()?;
329 Ok(db
330 .query_rows_into_cached::<Vec<Option<BookmarkData>>, _, _, _, _>(
331 &SEARCH_QUERY,
332 &[
333 (":search", &search as &dyn rusqlite::ToSql),
334 (":limit", &limit),
335 ],
336 |row| -> Result<_> {
337 scope.err_if_interrupted()?;
338 bookmark_from_row(row)
339 },
340 )?
341 .into_iter()
342 .flatten()
343 .collect())
344}
345
346pub fn recent_bookmarks(db: &PlacesDb, limit: u32) -> Result<Vec<BookmarkData>> {
347 let scope = db.begin_interrupt_scope()?;
348 Ok(db
349 .query_rows_into_cached::<Vec<Option<BookmarkData>>, _, _, _, _>(
350 &RECENT_BOOKMARKS_QUERY,
351 &[(":limit", &limit as &dyn rusqlite::ToSql)],
352 |row| -> Result<_> {
353 scope.err_if_interrupted()?;
354 bookmark_from_row(row)
355 },
356 )?
357 .into_iter()
358 .flatten()
359 .collect())
360}
361
362lazy_static::lazy_static! {
363 pub static ref SEARCH_QUERY: String = format!(
364 "SELECT
365 b.guid,
366 p.guid AS parentGuid,
367 b.position,
368 b.dateAdded,
369 b.lastModified,
370 -- Note we return null for titles with an empty string.
371 NULLIF(b.title, '') AS title,
372 h.url AS url
373 FROM moz_bookmarks b
374 JOIN moz_bookmarks p ON p.id = b.parent
375 JOIN moz_places h ON h.id = b.fk
376 WHERE b.type = {bookmark_type}
377 AND AUTOCOMPLETE_MATCH(
378 :search, h.url, IFNULL(b.title, h.title),
379 NULL, -- tags
380 -- We could pass the versions of these from history in,
381 -- but they're just used to figure out whether or not
382 -- the query fits the given behavior, and we know
383 -- we're only passing in and looking for bookmarks,
384 -- so using the args from history would be pointless
385 -- and would make things slower.
386 0, -- visit_count
387 0, -- typed
388 1, -- bookmarked
389 NULL, -- open page count
390 {match_bhvr},
391 {search_bhvr}
392 )
393 LIMIT :limit",
394 bookmark_type = BookmarkType::Bookmark as u8,
395 match_bhvr = crate::match_impl::MatchBehavior::Anywhere as u32,
396 search_bhvr = crate::match_impl::SearchBehavior::BOOKMARK.bits(),
397 );
398
399 pub static ref RECENT_BOOKMARKS_QUERY: String = format!(
400 "SELECT
401 b.guid,
402 p.guid AS parentGuid,
403 b.position,
404 b.dateAdded,
405 b.lastModified,
406 NULLIF(b.title, '') AS title,
407 h.url AS url
408 FROM moz_bookmarks b
409 JOIN moz_bookmarks p ON p.id = b.parent
410 JOIN moz_places h ON h.id = b.fk
411 WHERE b.type = {bookmark_type}
412 ORDER BY b.dateAdded DESC
413 LIMIT :limit",
414 bookmark_type = BookmarkType::Bookmark as u8
415 );
416}
417
418#[cfg(test)]
419mod test {
420 use super::*;
421 use crate::api::places_api::test::new_mem_connections;
422 use crate::tests::{append_invalid_bookmark, insert_json_tree};
423 use serde_json::json;
424 #[test]
425 fn test_get_by_url() -> Result<()> {
426 let conns = new_mem_connections();
427 insert_json_tree(
428 &conns.write,
429 json!({
430 "guid": String::from(BookmarkRootGuid::Unfiled.as_str()),
431 "children": [
432 {
433 "guid": "bookmark1___",
434 "url": "https://www.example1.com/",
435 "title": "no 1",
436 },
437 {
438 "guid": "bookmark2___",
439 "url": "https://www.example2.com/a/b/c/d?q=v#abcde",
440 "title": "yes 1",
441 },
442 {
443 "guid": "bookmark3___",
444 "url": "https://www.example2.com/a/b/c/d",
445 "title": "no 2",
446 },
447 {
448 "guid": "bookmark4___",
449 "url": "https://www.example2.com/a/b/c/d?q=v#abcde",
450 "title": "yes 2",
451 },
452 ]
453 }),
454 );
455 let url = url::Url::parse("https://www.example2.com/a/b/c/d?q=v#abcde")?;
456 let mut bmks = fetch_bookmarks_by_url(&conns.read, &url)?;
457 bmks.sort_by_key(|b| b.guid.as_str().to_string());
458 assert_eq!(bmks.len(), 2);
459 assert_eq!(
460 bmks[0],
461 BookmarkData {
462 guid: "bookmark2___".into(),
463 title: Some("yes 1".into()),
464 url: url.clone(),
465 parent_guid: BookmarkRootGuid::Unfiled.into(),
466 position: 1,
467 date_added: Timestamp(0),
469 last_modified: Timestamp(0),
470 }
471 );
472 assert_eq!(
473 bmks[1],
474 BookmarkData {
475 guid: "bookmark4___".into(),
476 title: Some("yes 2".into()),
477 url,
478 parent_guid: BookmarkRootGuid::Unfiled.into(),
479 position: 3,
480 date_added: Timestamp(0),
482 last_modified: Timestamp(0),
483 }
484 );
485
486 let no_url = url::Url::parse("https://no.bookmark.com")?;
487 assert!(fetch_bookmarks_by_url(&conns.read, &no_url)?.is_empty());
488
489 Ok(())
490 }
491 #[test]
492 fn test_search() -> Result<()> {
493 let conns = new_mem_connections();
494 insert_json_tree(
495 &conns.write,
496 json!({
497 "guid": String::from(BookmarkRootGuid::Unfiled.as_str()),
498 "children": [
499 {
500 "guid": "bookmark1___",
501 "url": "https://www.example1.com/",
502 "title": "",
503 },
504 {
505 "guid": "bookmark2___",
506 "url": "https://www.example2.com/a/b/c/d?q=v#example",
507 "title": "",
508 },
509 {
510 "guid": "bookmark3___",
511 "url": "https://www.example2.com/a/b/c/d",
512 "title": "",
513 },
514 {
515 "guid": "bookmark4___",
516 "url": "https://www.doesnt_match.com/a/b/c/d",
517 "title": "",
518 },
519 {
520 "guid": "bookmark5___",
521 "url": "https://www.example2.com/a/b/",
522 "title": "a b c d",
523 },
524 {
525 "guid": "bookmark6___",
526 "url": "https://www.example2.com/a/b/c/d",
527 "title": "foo bar baz",
528 },
529 {
530 "guid": "bookmark7___",
531 "url": "https://www.1234.com/a/b/c/d",
532 "title": "my example bookmark",
533 },
534 ]
535 }),
536 );
537 append_invalid_bookmark(
538 &conns.write,
539 BookmarkRootGuid::Unfiled.guid(),
540 "invalid",
541 "badurl",
542 );
543 let mut bmks = search_bookmarks(&conns.read, "ample", 10)?;
544 bmks.sort_by_key(|b| b.guid.as_str().to_string());
545 assert_eq!(bmks.len(), 6);
546 let expect = [
547 ("bookmark1___", "https://www.example1.com/", "", 0),
548 (
549 "bookmark2___",
550 "https://www.example2.com/a/b/c/d?q=v#example",
551 "",
552 1,
553 ),
554 ("bookmark3___", "https://www.example2.com/a/b/c/d", "", 2),
555 (
556 "bookmark5___",
557 "https://www.example2.com/a/b/",
558 "a b c d",
559 4,
560 ),
561 (
562 "bookmark6___",
563 "https://www.example2.com/a/b/c/d",
564 "foo bar baz",
565 5,
566 ),
567 (
568 "bookmark7___",
569 "https://www.1234.com/a/b/c/d",
570 "my example bookmark",
571 6,
572 ),
573 ];
574 for (got, want) in bmks.iter().zip(expect.iter()) {
575 assert_eq!(got.guid.as_str(), want.0);
576 assert_eq!(got.url, url::Url::parse(want.1).unwrap());
577 assert_eq!(got.title.as_ref().unwrap_or(&String::new()), want.2);
578 assert_eq!(got.position, want.3);
579 assert_eq!(got.parent_guid, BookmarkRootGuid::Unfiled);
580 }
581 Ok(())
582 }
583 #[test]
584 fn test_fetch_bookmark() -> Result<()> {
585 let conns = new_mem_connections();
586
587 insert_json_tree(
588 &conns.write,
589 json!({
590 "guid": BookmarkRootGuid::Mobile.as_guid(),
591 "children": [
592 {
593 "guid": "bookmark1___",
594 "url": "https://www.example1.com/"
595 },
596 {
597 "guid": "bookmark2___",
598 "url": "https://www.example2.com/"
599 },
600 ]
601 }),
602 );
603
604 let guid_bad = append_invalid_bookmark(
609 &conns.write,
610 BookmarkRootGuid::Mobile.guid(),
611 "invalid url",
612 "badurl",
613 )
614 .guid;
615 assert!(fetch_bookmark(&conns.read, &guid_bad, false)?.is_none());
616
617 let root = match fetch_bookmark(&conns.read, BookmarkRootGuid::Root.guid(), false)?.unwrap()
619 {
620 Item::Folder { f } => f,
621 _ => panic!("root not a folder?"),
622 };
623 assert!(root.child_guids.is_some());
624 assert!(root.child_nodes.is_none());
625 assert_eq!(root.child_guids.unwrap().len(), 4);
626
627 let root = match fetch_bookmark(&conns.read, BookmarkRootGuid::Root.guid(), true)?.unwrap()
628 {
629 Item::Folder { f } => f,
630 _ => panic!("not a folder?"),
631 };
632
633 assert!(root.child_nodes.is_some());
634 assert!(root.child_guids.is_some());
635 assert_eq!(
636 root.child_guids.unwrap(),
637 root.child_nodes
638 .as_ref()
639 .unwrap()
640 .iter()
641 .map(|c| c.guid().clone())
642 .collect::<Vec<SyncGuid>>()
643 );
644 let root_children = root.child_nodes.unwrap();
645 assert_eq!(root_children.len(), 4);
646 for child in root_children {
647 match child {
648 Item::Folder { f: child } => {
649 assert!(child.child_guids.is_some());
650 assert!(child.child_nodes.is_none());
651 if child.guid == BookmarkRootGuid::Mobile {
652 assert_eq!(
653 child.child_guids.unwrap(),
654 &[
655 SyncGuid::from("bookmark1___"),
656 SyncGuid::from("bookmark2___")
657 ]
658 );
659 }
660 }
661 _ => panic!("all root children should be folders"),
662 }
663 }
664
665 let unfiled =
666 match fetch_bookmark(&conns.read, BookmarkRootGuid::Unfiled.guid(), false)?.unwrap() {
667 Item::Folder { f } => f,
668 _ => panic!("not a folder?"),
669 };
670
671 assert!(unfiled.child_guids.is_some());
672 assert!(unfiled.child_nodes.is_none());
673 assert_eq!(unfiled.child_guids.unwrap().len(), 0);
674
675 let unfiled =
676 match fetch_bookmark(&conns.read, BookmarkRootGuid::Unfiled.guid(), true)?.unwrap() {
677 Item::Folder { f } => f,
678 _ => panic!("not a folder?"),
679 };
680 assert!(unfiled.child_guids.is_some());
681 assert!(unfiled.child_nodes.is_some());
682
683 assert_eq!(unfiled.child_nodes.unwrap().len(), 0);
684 assert_eq!(unfiled.child_guids.unwrap().len(), 0);
685
686 assert!(fetch_bookmark(&conns.read, &"not_exist___".into(), true)?.is_none());
687 Ok(())
688 }
689 #[test]
690 fn test_fetch_tree() -> Result<()> {
691 let conns = new_mem_connections();
692
693 insert_json_tree(
694 &conns.write,
695 json!({
696 "guid": BookmarkRootGuid::Mobile.as_guid(),
697 "children": [
698 {
699 "guid": "bookmark1___",
700 "url": "https://www.example1.com/"
701 },
702 {
703 "guid": "bookmark2___",
704 "url": "https://www.example2.com/"
705 },
706 ]
707 }),
708 );
709
710 append_invalid_bookmark(
711 &conns.write,
712 BookmarkRootGuid::Mobile.guid(),
713 "invalid url",
714 "badurl",
715 );
716
717 let root = match fetch_tree(&conns.read, BookmarkRootGuid::Root.guid())?.unwrap() {
718 Item::Folder { f } => f,
719 _ => panic!("root not a folder?"),
720 };
721 assert!(root.parent_guid.is_none());
722 assert_eq!(root.position, 0);
723
724 assert!(root.child_guids.is_some());
725 let children = root.child_nodes.as_ref().unwrap();
726 assert_eq!(
727 root.child_guids.unwrap(),
728 children
729 .iter()
730 .map(|c| c.guid().clone())
731 .collect::<Vec<SyncGuid>>()
732 );
733 let mut mobile_pos = None;
734 for (i, c) in children.iter().enumerate() {
735 assert_eq!(i as u32, *c.position());
736 assert_eq!(c.parent_guid().unwrap(), &root.guid);
737 match c {
738 Item::Folder { f } => {
739 if f.guid == BookmarkRootGuid::Mobile {
741 assert!(f.child_guids.is_some());
742 assert!(f.child_nodes.is_some());
743 let child_nodes = f.child_nodes.as_ref().unwrap();
744 assert_eq!(
745 f.child_guids.as_ref().unwrap(),
746 &child_nodes
747 .iter()
748 .map(|c| c.guid().clone())
749 .collect::<Vec<SyncGuid>>()
750 );
751 mobile_pos = Some(i as u32);
752 let b = match &child_nodes[0] {
753 Item::Bookmark { b } => b,
754 _ => panic!("expect a bookmark"),
755 };
756 assert_eq!(b.position, 0);
757 assert_eq!(b.guid, SyncGuid::from("bookmark1___"));
758 assert_eq!(b.url, Url::parse("https://www.example1.com/").unwrap());
759
760 let b = match &child_nodes[1] {
761 Item::Bookmark { b } => b,
762 _ => panic!("expect a bookmark"),
763 };
764 assert_eq!(b.position, 1);
765 assert_eq!(b.guid, SyncGuid::from("bookmark2___"));
766 assert_eq!(b.url, Url::parse("https://www.example2.com/").unwrap());
767 }
768 }
769 _ => panic!("unexpected type"),
770 }
771 }
772 let mobile = match fetch_tree(&conns.read, BookmarkRootGuid::Mobile.guid())?.unwrap() {
775 Item::Folder { f } => f,
776 _ => panic!("not a folder?"),
777 };
778 assert_eq!(mobile.parent_guid.unwrap(), BookmarkRootGuid::Root);
779 assert_eq!(mobile.position, mobile_pos.unwrap());
780
781 let bm1 = match fetch_tree(&conns.read, &SyncGuid::from("bookmark1___"))?.unwrap() {
782 Item::Bookmark { b } => b,
783 _ => panic!("not a bookmark?"),
784 };
785 assert_eq!(bm1.parent_guid, BookmarkRootGuid::Mobile);
786 assert_eq!(bm1.position, 0);
787
788 Ok(())
789 }
790 #[test]
791 fn test_recent() -> Result<()> {
792 let conns = new_mem_connections();
793 let kids = [
794 json!({
795 "guid": "bookmark1___",
796 "url": "https://www.example1.com/",
797 "title": "b1",
798 }),
799 json!({
800 "guid": "bookmark2___",
801 "url": "https://www.example2.com/",
802 "title": "b2",
803 }),
804 json!({
805 "guid": "bookmark3___",
806 "url": "https://www.example3.com/",
807 "title": "b3",
808 }),
809 json!({
810 "guid": "bookmark4___",
811 "url": "https://www.example4.com/",
812 "title": "b4",
813 }),
814 json!({
816 "guid": "folder1_____",
817 "title": "A folder",
818 "children": []
819 }),
820 json!({
821 "guid": "bookmark5___",
822 "url": "https://www.example5.com/",
823 "title": "b5",
824 }),
825 ];
826 for k in &kids {
827 insert_json_tree(
828 &conns.write,
829 json!({
830 "guid": String::from(BookmarkRootGuid::Unfiled.as_str()),
831 "children": [k.clone()],
832 }),
833 );
834 std::thread::sleep(std::time::Duration::from_millis(10));
835 }
836
837 append_invalid_bookmark(
838 &conns.write,
839 BookmarkRootGuid::Unfiled.guid(),
840 "invalid url",
841 "badurl",
842 );
843
844 let bmks = recent_bookmarks(&conns.read, 4)?;
846 assert_eq!(bmks.len(), 3);
847
848 assert_eq!(
849 bmks[0],
850 BookmarkData {
851 guid: "bookmark5___".into(),
852 title: Some("b5".into()),
853 url: Url::parse("https://www.example5.com/").unwrap(),
854 parent_guid: BookmarkRootGuid::Unfiled.into(),
855 position: 5,
856 date_added: Timestamp(0),
858 last_modified: Timestamp(0),
859 }
860 );
861 assert_eq!(
862 bmks[1],
863 BookmarkData {
864 guid: "bookmark4___".into(),
865 title: Some("b4".into()),
866 url: Url::parse("https://www.example4.com/").unwrap(),
867 parent_guid: BookmarkRootGuid::Unfiled.into(),
868 position: 3,
869 date_added: Timestamp(0),
871 last_modified: Timestamp(0),
872 }
873 );
874 assert_eq!(
875 bmks[2],
876 BookmarkData {
877 guid: "bookmark3___".into(),
878 title: Some("b3".into()),
879 url: Url::parse("https://www.example3.com/").unwrap(),
880 parent_guid: BookmarkRootGuid::Unfiled.into(),
881 position: 2,
882 date_added: Timestamp(0),
884 last_modified: Timestamp(0),
885 }
886 );
887 Ok(())
888 }
889}