places/storage/bookmarks/
json_tree.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5// This supports inserting and fetching an entire bookmark tree via JSON
6// compatible data structures.
7// It's currently used only by tests, examples and our utilities for importing
8// from a desktop JSON exports.
9//
10// None of our "real" consumers currently require JSON compatibility, so try
11// and avoid using this if you can!
12// (We could possibly put this behind a feature flag?)
13
14use crate::error::{warn, Result};
15use crate::types::BookmarkType;
16//#[cfg(test)]
17use crate::db::PlacesDb;
18use rusqlite::Row;
19use sql_support::ConnExt;
20use std::collections::HashMap;
21use sync_guid::Guid as SyncGuid;
22use types::Timestamp;
23use url::Url;
24
25use super::{
26    BookmarkPosition, InsertableBookmark, InsertableFolder, InsertableItem, InsertableSeparator,
27    RowId,
28};
29
30use serde::{
31    de::{Deserialize, Deserializer},
32    ser::{Serialize, SerializeStruct, Serializer},
33};
34use serde_derive::*;
35
36/// Support for inserting and fetching a tree. Same limitations as desktop.
37/// Note that the guids are optional when inserting a tree. They will always
38/// have values when fetching it.
39// For testing purposes we implement PartialEq, such that optional fields are
40// ignored in the comparison. This allows tests to construct a tree with
41// missing fields and be able to compare against a tree with all fields (such
42// as one exported from the DB)
43#[cfg(test)]
44fn cmp_options<T: PartialEq>(s: &Option<T>, o: &Option<T>) -> bool {
45    match (s, o) {
46        (None, None) => true,
47        (None, Some(_)) => true,
48        (Some(_), None) => true,
49        (s, o) => s == o,
50    }
51}
52
53#[derive(Debug)]
54pub struct BookmarkNode {
55    pub guid: Option<SyncGuid>,
56    pub date_added: Option<Timestamp>,
57    pub last_modified: Option<Timestamp>,
58    pub title: Option<String>,
59    pub url: Url,
60}
61
62impl From<BookmarkNode> for BookmarkTreeNode {
63    fn from(b: BookmarkNode) -> Self {
64        BookmarkTreeNode::Bookmark { b }
65    }
66}
67
68#[cfg(test)]
69impl PartialEq for BookmarkNode {
70    fn eq(&self, other: &BookmarkNode) -> bool {
71        cmp_options(&self.guid, &other.guid)
72            && cmp_options(&self.date_added, &other.date_added)
73            && cmp_options(&self.last_modified, &other.last_modified)
74            && cmp_options(&self.title, &other.title)
75            && self.url == other.url
76    }
77}
78
79#[derive(Debug, Default)]
80pub struct SeparatorNode {
81    pub guid: Option<SyncGuid>,
82    pub date_added: Option<Timestamp>,
83    pub last_modified: Option<Timestamp>,
84}
85
86impl From<SeparatorNode> for BookmarkTreeNode {
87    fn from(s: SeparatorNode) -> Self {
88        BookmarkTreeNode::Separator { s }
89    }
90}
91
92#[cfg(test)]
93impl PartialEq for SeparatorNode {
94    fn eq(&self, other: &SeparatorNode) -> bool {
95        cmp_options(&self.guid, &other.guid)
96            && cmp_options(&self.date_added, &other.date_added)
97            && cmp_options(&self.last_modified, &other.last_modified)
98    }
99}
100
101#[derive(Debug, Default)]
102pub struct FolderNode {
103    pub guid: Option<SyncGuid>,
104    pub date_added: Option<Timestamp>,
105    pub last_modified: Option<Timestamp>,
106    pub title: Option<String>,
107    pub children: Vec<BookmarkTreeNode>,
108}
109
110impl From<FolderNode> for BookmarkTreeNode {
111    fn from(f: FolderNode) -> Self {
112        BookmarkTreeNode::Folder { f }
113    }
114}
115
116#[cfg(test)]
117impl PartialEq for FolderNode {
118    fn eq(&self, other: &FolderNode) -> bool {
119        cmp_options(&self.guid, &other.guid)
120            && cmp_options(&self.date_added, &other.date_added)
121            && cmp_options(&self.last_modified, &other.last_modified)
122            && cmp_options(&self.title, &other.title)
123            && self.children == other.children
124    }
125}
126
127#[derive(Debug)]
128#[cfg_attr(test, derive(PartialEq))]
129pub enum BookmarkTreeNode {
130    Bookmark { b: BookmarkNode },
131    Separator { s: SeparatorNode },
132    Folder { f: FolderNode },
133}
134
135impl BookmarkTreeNode {
136    pub fn node_type(&self) -> BookmarkType {
137        match self {
138            BookmarkTreeNode::Bookmark { .. } => BookmarkType::Bookmark,
139            BookmarkTreeNode::Folder { .. } => BookmarkType::Folder,
140            BookmarkTreeNode::Separator { .. } => BookmarkType::Separator,
141        }
142    }
143
144    pub fn guid(&self) -> &SyncGuid {
145        let guid = match self {
146            BookmarkTreeNode::Bookmark { b } => b.guid.as_ref(),
147            BookmarkTreeNode::Folder { f } => f.guid.as_ref(),
148            BookmarkTreeNode::Separator { s } => s.guid.as_ref(),
149        };
150        // Can this happen? Why is this an Option?
151        guid.expect("Missing guid?")
152    }
153
154    pub fn created_modified(&self) -> (Timestamp, Timestamp) {
155        let (created, modified) = match self {
156            BookmarkTreeNode::Bookmark { b } => (b.date_added, b.last_modified),
157            BookmarkTreeNode::Folder { f } => (f.date_added, f.last_modified),
158            BookmarkTreeNode::Separator { s } => (s.date_added, s.last_modified),
159        };
160        (
161            created.unwrap_or_else(Timestamp::now),
162            modified.unwrap_or_else(Timestamp::now),
163        )
164    }
165}
166
167// Serde makes it tricky to serialize what we need here - a 'type' from the
168// enum and then a flattened variant struct. So we gotta do it manually.
169impl Serialize for BookmarkTreeNode {
170    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
171    where
172        S: Serializer,
173    {
174        let mut state = serializer.serialize_struct("BookmarkTreeNode", 2)?;
175        match self {
176            BookmarkTreeNode::Bookmark { b } => {
177                state.serialize_field("type", &BookmarkType::Bookmark)?;
178                state.serialize_field("guid", &b.guid)?;
179                state.serialize_field("date_added", &b.date_added)?;
180                state.serialize_field("last_modified", &b.last_modified)?;
181                state.serialize_field("title", &b.title)?;
182                state.serialize_field("url", &b.url.to_string())?;
183            }
184            BookmarkTreeNode::Separator { s } => {
185                state.serialize_field("type", &BookmarkType::Separator)?;
186                state.serialize_field("guid", &s.guid)?;
187                state.serialize_field("date_added", &s.date_added)?;
188                state.serialize_field("last_modified", &s.last_modified)?;
189            }
190            BookmarkTreeNode::Folder { f } => {
191                state.serialize_field("type", &BookmarkType::Folder)?;
192                state.serialize_field("guid", &f.guid)?;
193                state.serialize_field("date_added", &f.date_added)?;
194                state.serialize_field("last_modified", &f.last_modified)?;
195                state.serialize_field("title", &f.title)?;
196                state.serialize_field("children", &f.children)?;
197            }
198        };
199        state.end()
200    }
201}
202
203impl<'de> Deserialize<'de> for BookmarkTreeNode {
204    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
205    where
206        D: Deserializer<'de>,
207    {
208        // *sob* - a union of fields we post-process.
209        #[derive(Debug, Default, Deserialize)]
210        #[serde(default)]
211        struct Mapping {
212            #[serde(rename = "type")]
213            bookmark_type: u8,
214            guid: Option<SyncGuid>,
215            date_added: Option<Timestamp>,
216            last_modified: Option<Timestamp>,
217            title: Option<String>,
218            url: Option<String>,
219            children: Vec<BookmarkTreeNode>,
220        }
221        let m = Mapping::deserialize(deserializer)?;
222
223        let url = m.url.as_ref().and_then(|u| match Url::parse(u) {
224            Err(e) => {
225                warn!(
226                    "ignoring invalid url for {}: {:?}",
227                    m.guid.as_ref().map(AsRef::as_ref).unwrap_or("<no guid>"),
228                    e
229                );
230                None
231            }
232            Ok(parsed) => Some(parsed),
233        });
234
235        let bookmark_type = BookmarkType::from_u8_with_valid_url(m.bookmark_type, || url.is_some());
236        Ok(match bookmark_type {
237            BookmarkType::Bookmark => BookmarkNode {
238                guid: m.guid,
239                date_added: m.date_added,
240                last_modified: m.last_modified,
241                title: m.title,
242                url: url.unwrap(),
243            }
244            .into(),
245            BookmarkType::Separator => SeparatorNode {
246                guid: m.guid,
247                date_added: m.date_added,
248                last_modified: m.last_modified,
249            }
250            .into(),
251            BookmarkType::Folder => FolderNode {
252                guid: m.guid,
253                date_added: m.date_added,
254                last_modified: m.last_modified,
255                title: m.title,
256                children: m.children,
257            }
258            .into(),
259        })
260    }
261}
262
263impl From<BookmarkTreeNode> for InsertableItem {
264    fn from(node: BookmarkTreeNode) -> Self {
265        match node {
266            BookmarkTreeNode::Bookmark { b } => InsertableBookmark {
267                parent_guid: SyncGuid::empty(),
268                position: BookmarkPosition::Append,
269                date_added: b.date_added,
270                last_modified: b.last_modified,
271                guid: b.guid,
272                url: b.url,
273                title: b.title,
274            }
275            .into(),
276            BookmarkTreeNode::Separator { s } => InsertableSeparator {
277                parent_guid: SyncGuid::empty(),
278                position: BookmarkPosition::Append,
279                date_added: s.date_added,
280                last_modified: s.last_modified,
281                guid: s.guid,
282            }
283            .into(),
284            BookmarkTreeNode::Folder { f } => InsertableFolder {
285                parent_guid: SyncGuid::empty(),
286                position: BookmarkPosition::Append,
287                date_added: f.date_added,
288                last_modified: f.last_modified,
289                guid: f.guid,
290                title: f.title,
291                children: f.children.into_iter().map(Into::into).collect(),
292            }
293            .into(),
294        }
295    }
296}
297
298#[cfg(test)]
299mod test_serialize {
300    use super::*;
301    use serde_json::json;
302
303    #[test]
304    fn test_tree_serialize() -> Result<()> {
305        let guid = SyncGuid::random();
306        let tree = BookmarkTreeNode::Folder {
307            f: FolderNode {
308                guid: Some(guid.clone()),
309                date_added: None,
310                last_modified: None,
311                title: None,
312                children: vec![BookmarkTreeNode::Bookmark {
313                    b: BookmarkNode {
314                        guid: None,
315                        date_added: None,
316                        last_modified: None,
317                        title: Some("the bookmark".into()),
318                        url: Url::parse("https://www.example.com")?,
319                    },
320                }],
321            },
322        };
323        // round-trip the tree via serde.
324        let json = serde_json::to_string_pretty(&tree)?;
325        let deser: BookmarkTreeNode = serde_json::from_str(&json)?;
326        assert_eq!(tree, deser);
327        // and check against the simplest json repr of the tree, which checks
328        // our PartialEq implementation.
329        let jtree = json!({
330            "type": 2,
331            "guid": &guid,
332            "children" : [
333                {
334                    "type": 1,
335                    "title": "the bookmark",
336                    "url": "https://www.example.com/"
337                }
338            ]
339        });
340        let deser_tree: BookmarkTreeNode = serde_json::from_value(jtree).expect("should deser");
341        assert_eq!(tree, deser_tree);
342        Ok(())
343    }
344
345    #[test]
346    fn test_tree_invalid() {
347        let jtree = json!({
348            "type": 2,
349            "children" : [
350                {
351                    "type": 1,
352                    "title": "bookmark with invalid URL",
353                    "url": "invalid_url"
354                },
355                {
356                    "type": 1,
357                    "title": "bookmark with missing URL",
358                },
359                {
360                    "title": "bookmark with missing type, no URL",
361                },
362                {
363                    "title": "bookmark with missing type, valid URL",
364                    "url": "http://example.com"
365                },
366
367            ]
368        });
369        let deser_tree: BookmarkTreeNode = serde_json::from_value(jtree).expect("should deser");
370        let folder = match deser_tree {
371            BookmarkTreeNode::Folder { f } => f,
372            _ => panic!("must be a folder"),
373        };
374
375        let children = folder.children;
376        assert_eq!(children.len(), 4);
377
378        assert!(match &children[0] {
379            BookmarkTreeNode::Folder { f } =>
380                f.title == Some("bookmark with invalid URL".to_string()),
381            _ => false,
382        });
383        assert!(match &children[1] {
384            BookmarkTreeNode::Folder { f } =>
385                f.title == Some("bookmark with missing URL".to_string()),
386            _ => false,
387        });
388        assert!(match &children[2] {
389            BookmarkTreeNode::Folder { f } => {
390                f.title == Some("bookmark with missing type, no URL".to_string())
391            }
392            _ => false,
393        });
394        assert!(match &children[3] {
395            BookmarkTreeNode::Bookmark { b } => {
396                b.title == Some("bookmark with missing type, valid URL".to_string())
397            }
398            _ => false,
399        });
400    }
401}
402
403pub fn insert_tree(db: &PlacesDb, tree: FolderNode) -> Result<()> {
404    // This API is strange - we don't add `tree`, but just use it for the parent.
405    // It's only used for json importing, so we can live with a strange API :)
406    let parent = tree.guid.expect("inserting a tree without the root guid");
407    let tx = db.begin_transaction()?;
408    for child in tree.children {
409        let mut insertable: InsertableItem = child.into();
410        assert!(
411            insertable.parent_guid().is_empty(),
412            "can't specify a parent inserting a tree"
413        );
414        insertable.set_parent_guid(parent.clone());
415        crate::storage::bookmarks::insert_bookmark_in_tx(db, insertable)?;
416    }
417    crate::storage::delete_pending_temp_tables(db)?;
418    tx.commit()?;
419    Ok(())
420}
421
422fn inflate(
423    parent: &mut BookmarkTreeNode,
424    pseudo_tree: &mut HashMap<SyncGuid, Vec<BookmarkTreeNode>>,
425) {
426    if let BookmarkTreeNode::Folder { f: parent } = parent {
427        if let Some(children) = parent
428            .guid
429            .as_ref()
430            .and_then(|guid| pseudo_tree.remove(guid))
431        {
432            parent.children = children;
433            for child in &mut parent.children {
434                inflate(child, pseudo_tree);
435            }
436        }
437    }
438}
439
440#[derive(Debug)]
441struct FetchedTreeRow {
442    level: u32,
443    _id: RowId,
444    guid: SyncGuid,
445    // parent and parent_guid are Option<> only to handle the root - we would
446    // assert but they aren't currently used.
447    _parent: Option<RowId>,
448    parent_guid: Option<SyncGuid>,
449    node_type: BookmarkType,
450    position: u32,
451    title: Option<String>,
452    date_added: Timestamp,
453    last_modified: Timestamp,
454    url: Option<String>,
455}
456
457impl FetchedTreeRow {
458    pub fn from_row(row: &Row<'_>) -> Result<Self> {
459        let url = row.get::<_, Option<String>>("url")?;
460        Ok(Self {
461            level: row.get("level")?,
462            _id: row.get::<_, RowId>("id")?,
463            guid: row.get::<_, String>("guid")?.into(),
464            _parent: row.get::<_, Option<RowId>>("parent")?,
465            parent_guid: row
466                .get::<_, Option<String>>("parentGuid")?
467                .map(SyncGuid::from),
468            node_type: BookmarkType::from_u8_with_valid_url(row.get::<_, u8>("type")?, || {
469                url.is_some()
470            }),
471            position: row.get("position")?,
472            title: row.get::<_, Option<String>>("title")?,
473            date_added: row.get("dateAdded")?,
474            last_modified: row.get("lastModified")?,
475            url,
476        })
477    }
478}
479
480/// Fetch the tree starting at the specified guid.
481/// Returns a `BookmarkTreeNode`, its parent's guid (if any), and
482/// position inside its parent.
483pub enum FetchDepth {
484    Specific(usize),
485    Deepest,
486}
487
488pub fn fetch_tree(
489    db: &PlacesDb,
490    item_guid: &SyncGuid,
491    target_depth: &FetchDepth,
492) -> Result<Option<(BookmarkTreeNode, Option<SyncGuid>, u32)>> {
493    // XXX - this needs additional work for tags - unlike desktop, there's no
494    // "tags" folder, but instead a couple of tables to join on.
495    let sql = r#"
496        WITH RECURSIVE
497        descendants(fk, level, type, id, guid, parent, parentGuid, position,
498                    title, dateAdded, lastModified) AS (
499        SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
500                (SELECT guid FROM moz_bookmarks WHERE id = b1.parent),
501                b1.position, b1.title, b1.dateAdded, b1.lastModified
502        FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
503        UNION ALL
504        SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
505                descendants.guid, b2.position, b2.title, b2.dateAdded,
506                b2.lastModified
507        FROM moz_bookmarks b2
508        JOIN descendants ON b2.parent = descendants.id) -- AND b2.id <> :tags_folder)
509        SELECT d.level, d.id, d.guid, d.parent, d.parentGuid, d.type,
510            d.position, NULLIF(d.title, '') AS title, d.dateAdded,
511            d.lastModified, h.url
512--               (SELECT icon_url FROM moz_icons i
513--                      JOIN moz_icons_to_pages ON icon_id = i.id
514--                      JOIN moz_pages_w_icons pi ON page_id = pi.id
515--                      WHERE pi.page_url_hash = hash(h.url) AND pi.page_url = h.url
516--                      ORDER BY width DESC LIMIT 1) AS iconuri,
517--               (SELECT GROUP_CONCAT(t.title, ',')
518--                FROM moz_bookmarks b2
519--                JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder
520--                WHERE b2.fk = h.id
521--               ) AS tags,
522--               EXISTS (SELECT 1 FROM moz_items_annos
523--                       WHERE item_id = d.id LIMIT 1) AS has_annos,
524--               (SELECT a.content FROM moz_annos a
525--                JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
526--                WHERE place_id = h.id AND n.name = :charset_anno
527--               ) AS charset
528        FROM descendants d
529        LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent
530        LEFT JOIN moz_places h ON h.id = d.fk
531        ORDER BY d.level, d.parent, d.position"#;
532
533    let scope = db.begin_interrupt_scope()?;
534
535    let mut stmt = db.conn().prepare(sql)?;
536
537    let mut results =
538        stmt.query_and_then(&[(":item_guid", item_guid)], FetchedTreeRow::from_row)?;
539
540    let parent_guid: Option<SyncGuid>;
541    let position: u32;
542
543    // The first row in the result set is always the root of our tree.
544    let mut root = match results.next() {
545        Some(result) => {
546            let row = result?;
547            parent_guid = row.parent_guid.clone();
548            position = row.position;
549            match row.node_type {
550                BookmarkType::Folder => FolderNode {
551                    guid: Some(row.guid.clone()),
552                    date_added: Some(row.date_added),
553                    last_modified: Some(row.last_modified),
554                    title: row.title,
555                    children: Vec::new(),
556                }
557                .into(),
558                BookmarkType::Bookmark => {
559                    // pretend invalid or missing URLs don't exist.
560                    match row.url {
561                        Some(str_val) => match Url::parse(str_val.as_str()) {
562                            // an invalid URL presumably means a logic error
563                            // somewhere far away from here...
564                            Err(_) => return Ok(None),
565                            Ok(url) => BookmarkNode {
566                                guid: Some(row.guid.clone()),
567                                date_added: Some(row.date_added),
568                                last_modified: Some(row.last_modified),
569                                title: row.title,
570                                url,
571                            }
572                            .into(),
573                        },
574                        // This is double-extra-invalid because various
575                        // constraints in the schema should prevent it (but we
576                        // know from desktop's experience that on-disk
577                        // corruption can cause it, so it's possible) - but
578                        // we treat it as an `error` rather than just a `warn`
579                        None => {
580                            error_support::report_error!(
581                                "places-bookmark-corruption",
582                                "bookmark {:#} has missing url",
583                                row.guid
584                            );
585                            return Ok(None);
586                        }
587                    }
588                }
589                BookmarkType::Separator => SeparatorNode {
590                    guid: Some(row.guid.clone()),
591                    date_added: Some(row.date_added),
592                    last_modified: Some(row.last_modified),
593                }
594                .into(),
595            }
596        }
597        None => return Ok(None),
598    };
599
600    // Skip the rest and return if root is not a folder
601    if let BookmarkTreeNode::Bookmark { .. } | BookmarkTreeNode::Separator { .. } = root {
602        return Ok(Some((root, parent_guid, position)));
603    }
604
605    scope.err_if_interrupted()?;
606    // For all remaining rows, build a pseudo-tree that maps parent GUIDs to
607    // ordered children. We need this intermediate step because SQLite returns
608    // results in level order, so we'll see a node's siblings and cousins (same
609    // level, but different parents) before any of their descendants.
610    let mut pseudo_tree: HashMap<SyncGuid, Vec<BookmarkTreeNode>> = HashMap::new();
611    for result in results {
612        let row = result?;
613        scope.err_if_interrupted()?;
614        // Check if we have done fetching the asked depth
615        if let FetchDepth::Specific(d) = *target_depth {
616            if row.level as usize > d + 1 {
617                break;
618            }
619        }
620        let node = match row.node_type {
621            BookmarkType::Bookmark => match &row.url {
622                Some(url_str) => match Url::parse(url_str) {
623                    Ok(url) => BookmarkNode {
624                        guid: Some(row.guid.clone()),
625                        date_added: Some(row.date_added),
626                        last_modified: Some(row.last_modified),
627                        title: row.title.clone(),
628                        url,
629                    }
630                    .into(),
631                    Err(e) => {
632                        warn!(
633                            "ignoring malformed bookmark {} - invalid URL: {:?}",
634                            row.guid, e
635                        );
636                        continue;
637                    }
638                },
639                None => {
640                    warn!("ignoring malformed bookmark {} - no URL", row.guid);
641                    continue;
642                }
643            },
644            BookmarkType::Separator => SeparatorNode {
645                guid: Some(row.guid.clone()),
646                date_added: Some(row.date_added),
647                last_modified: Some(row.last_modified),
648            }
649            .into(),
650            BookmarkType::Folder => FolderNode {
651                guid: Some(row.guid.clone()),
652                date_added: Some(row.date_added),
653                last_modified: Some(row.last_modified),
654                title: row.title.clone(),
655                children: Vec::new(),
656            }
657            .into(),
658        };
659        if let Some(parent_guid) = row.parent_guid.as_ref().cloned() {
660            let children = pseudo_tree.entry(parent_guid).or_default();
661            children.push(node);
662        }
663    }
664
665    // Finally, inflate our tree.
666    inflate(&mut root, &mut pseudo_tree);
667    Ok(Some((root, parent_guid, position)))
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use crate::api::places_api::test::new_mem_connection;
674    use crate::storage::bookmarks::BookmarkRootGuid;
675    use crate::tests::{assert_json_tree, assert_json_tree_with_depth};
676    use serde_json::json;
677
678    // These tests check the SQL that this JSON module does "behind the back" of the
679    // main storage API.
680    #[test]
681    fn test_fetch_root() -> Result<()> {
682        let conn = new_mem_connection();
683
684        // Fetch the root
685        let (t, _, _) =
686            fetch_tree(&conn, &BookmarkRootGuid::Root.into(), &FetchDepth::Deepest)?.unwrap();
687        let f = match t {
688            BookmarkTreeNode::Folder { ref f } => f,
689            _ => panic!("tree root must be a folder"),
690        };
691        assert_eq!(f.guid, Some(BookmarkRootGuid::Root.into()));
692        assert_eq!(f.children.len(), 4);
693        Ok(())
694    }
695
696    #[test]
697    fn test_insert_tree_and_fetch_level() -> Result<()> {
698        let conn = new_mem_connection();
699
700        let tree = FolderNode {
701            guid: Some(BookmarkRootGuid::Unfiled.into()),
702            children: vec![
703                BookmarkNode {
704                    guid: None,
705                    date_added: None,
706                    last_modified: None,
707                    title: Some("the bookmark".into()),
708                    url: Url::parse("https://www.example.com")?,
709                }
710                .into(),
711                FolderNode {
712                    title: Some("A folder".into()),
713                    children: vec![
714                        BookmarkNode {
715                            guid: None,
716                            date_added: None,
717                            last_modified: None,
718                            title: Some("bookmark 1 in A folder".into()),
719                            url: Url::parse("https://www.example2.com")?,
720                        }
721                        .into(),
722                        BookmarkNode {
723                            guid: None,
724                            date_added: None,
725                            last_modified: None,
726                            title: Some("bookmark 2 in A folder".into()),
727                            url: Url::parse("https://www.example3.com")?,
728                        }
729                        .into(),
730                    ],
731                    ..Default::default()
732                }
733                .into(),
734                BookmarkNode {
735                    guid: None,
736                    date_added: None,
737                    last_modified: None,
738                    title: Some("another bookmark".into()),
739                    url: Url::parse("https://www.example4.com")?,
740                }
741                .into(),
742            ],
743            ..Default::default()
744        };
745        insert_tree(&conn, tree)?;
746
747        let expected = json!({
748            "guid": &BookmarkRootGuid::Unfiled.as_guid(),
749            "children": [
750                {
751                    "title": "the bookmark",
752                    "url": "https://www.example.com/"
753                },
754                {
755                    "title": "A folder",
756                    "children": [
757                        {
758                            "title": "bookmark 1 in A folder",
759                            "url": "https://www.example2.com/"
760                        },
761                        {
762                            "title": "bookmark 2 in A folder",
763                            "url": "https://www.example3.com/"
764                        }
765                    ],
766                },
767                {
768                    "title": "another bookmark",
769                    "url": "https://www.example4.com/",
770                }
771            ]
772        });
773        // check it with deepest fetching level.
774        assert_json_tree(&conn, &BookmarkRootGuid::Unfiled.into(), expected.clone());
775
776        // check it with one level deep, which should be the same as the previous
777        assert_json_tree_with_depth(
778            &conn,
779            &BookmarkRootGuid::Unfiled.into(),
780            expected,
781            &FetchDepth::Specific(1),
782        );
783
784        // check it with zero level deep, which should return root and its children only
785        assert_json_tree_with_depth(
786            &conn,
787            &BookmarkRootGuid::Unfiled.into(),
788            json!({
789                "guid": &BookmarkRootGuid::Unfiled.as_guid(),
790                "children": [
791                    {
792                        "title": "the bookmark",
793                        "url": "https://www.example.com/"
794                    },
795                    {
796                        "title": "A folder",
797                        "children": [],
798                    },
799                    {
800                        "title": "another bookmark",
801                        "url": "https://www.example4.com/",
802                    }
803                ]
804            }),
805            &FetchDepth::Specific(0),
806        );
807
808        Ok(())
809    }
810}