summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/maintenance/test_preventive_maintenance.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/maintenance/test_preventive_maintenance.js')
-rw-r--r--toolkit/components/places/tests/maintenance/test_preventive_maintenance.js2823
1 files changed, 2823 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/maintenance/test_preventive_maintenance.js b/toolkit/components/places/tests/maintenance/test_preventive_maintenance.js
new file mode 100644
index 0000000000..a1ae830d8e
--- /dev/null
+++ b/toolkit/components/places/tests/maintenance/test_preventive_maintenance.js
@@ -0,0 +1,2823 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test preventive maintenance
+ * For every maintenance query create an uncoherent db and check that we take
+ * correct fix steps, without polluting valid data.
+ */
+
+// ------------------------------------------------------------------------------
+// Helpers
+
+var defaultBookmarksMaxId = 0;
+async function cleanDatabase() {
+ // First clear any bookmarks the "proper way" to ensure caches like GuidHelper
+ // are properly cleared.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.withConnectionWrapper("cleanDatabase", async db => {
+ await db.executeTransaction(async () => {
+ await db.executeCached("DELETE FROM moz_places");
+ await db.executeCached("DELETE FROM moz_origins");
+ await db.executeCached("DELETE FROM moz_historyvisits");
+ await db.executeCached("DELETE FROM moz_anno_attributes");
+ await db.executeCached("DELETE FROM moz_annos");
+ await db.executeCached("DELETE FROM moz_inputhistory");
+ await db.executeCached("DELETE FROM moz_keywords");
+ await db.executeCached("DELETE FROM moz_icons");
+ await db.executeCached("DELETE FROM moz_pages_w_icons");
+ await db.executeCached(
+ "DELETE FROM moz_bookmarks WHERE id > " + defaultBookmarksMaxId
+ );
+ await db.executeCached("DELETE FROM moz_bookmarks_deleted");
+ await db.executeCached("DELETE FROM moz_places_metadata_search_queries");
+ });
+ });
+ // Since we're doing raw deletes, we must invalidate the guids cache.
+ await PlacesUtils.invalidateCachedGuids();
+}
+
+async function addPlace(
+ aUrl,
+ aFavicon,
+ aGuid = PlacesUtils.history.makeGuid(),
+ aHash = null
+) {
+ let href = new URL(
+ aUrl || `http://www.mozilla.org/${encodeURIComponent(aGuid)}`
+ ).href;
+ let id;
+ await PlacesUtils.withConnectionWrapper("cleanDatabase", async db => {
+ await db.executeTransaction(async () => {
+ id = (
+ await db.executeCached(
+ `INSERT INTO moz_places (url, url_hash, guid)
+ VALUES (:url, IFNULL(:hash, hash(:url)), :guid)
+ RETURNING id`,
+ {
+ url: href,
+ hash: aHash,
+ guid: aGuid,
+ }
+ )
+ )[0].getResultByIndex(0);
+ await db.executeCached("DELETE FROM moz_updateoriginsinsert_temp");
+
+ if (aFavicon) {
+ await db.executeCached(
+ `INSERT INTO moz_pages_w_icons (page_url, page_url_hash)
+ VALUES (:url, IFNULL(:hash, hash(:url)))`,
+ {
+ url: href,
+ hash: aHash,
+ }
+ );
+ await db.executeCached(
+ `INSERT INTO moz_icons_to_pages (page_id, icon_id)
+ VALUES (
+ (SELECT id FROM moz_pages_w_icons WHERE page_url_hash = IFNULL(:hash, hash(:url))),
+ :favicon
+ )`,
+ {
+ url: href,
+ hash: aHash,
+ favicon: aFavicon,
+ }
+ );
+ }
+ });
+ });
+ return id;
+}
+
+async function addBookmark(
+ aPlaceId,
+ aType,
+ aParentGuid = PlacesUtils.bookmarks.unfiledGuid,
+ aKeywordId,
+ aTitle,
+ aGuid = PlacesUtils.history.makeGuid(),
+ aSyncStatus = PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ aSyncChangeCounter = 0
+) {
+ return PlacesUtils.withConnectionWrapper("addBookmark", async db => {
+ return (
+ await db.executeCached(
+ `INSERT INTO moz_bookmarks (fk, type, parent, keyword_id,
+ title, guid, syncStatus, syncChangeCounter)
+ VALUES (:place_id, :type,
+ (SELECT id FROM moz_bookmarks WHERE guid = :parent), :keyword_id,
+ :title, :guid, :sync_status, :change_counter)
+ RETURNING id`,
+ {
+ place_id: aPlaceId || null,
+ type: aType || null,
+ parent: aParentGuid,
+ keyword_id: aKeywordId || null,
+ title: typeof aTitle == "string" ? aTitle : null,
+ guid: aGuid,
+ sync_status: aSyncStatus,
+ change_counter: aSyncChangeCounter,
+ }
+ )
+ )[0].getResultByIndex(0);
+ });
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+
+var tests = [];
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "A.1",
+ desc: "Remove obsolete annotations from moz_annos",
+
+ _obsoleteWeaveAttribute: "weave/test",
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid.
+ this._placeId = await addPlace();
+ // Add an obsolete attribute.
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeTransaction(async () => {
+ db.executeCached(
+ "INSERT INTO moz_anno_attributes (name) VALUES (:anno)",
+ { anno: this._obsoleteWeaveAttribute }
+ );
+
+ db.executeCached(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES (:place_id,
+ (SELECT id FROM moz_anno_attributes WHERE name = :anno)
+ )`,
+ {
+ place_id: this._placeId,
+ anno: this._obsoleteWeaveAttribute,
+ }
+ );
+ });
+ });
+ },
+
+ async check() {
+ // Check that the obsolete annotation has been removed.
+ Assert.strictEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_anno_attributes", "id", {
+ name: this._obsoleteWeaveAttribute,
+ }),
+ undefined
+ );
+ },
+});
+
+tests.push({
+ name: "A.3",
+ desc: "Remove unused attributes",
+
+ _usedPageAttribute: "usedPage",
+ _unusedAttribute: "unused",
+ _placeId: null,
+ _bookmarkId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // add a bookmark
+ this._bookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ );
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeTransaction(async () => {
+ // Add a used attribute and an unused one.
+ await db.executeCached(
+ `INSERT INTO moz_anno_attributes (name)
+ VALUES (:anno1), (:anno2)`,
+ {
+ anno1: this._usedPageAttribute,
+ anno2: this._unusedAttribute,
+ }
+ );
+ await db.executeCached(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))`,
+ {
+ place_id: this._placeId,
+ anno: this._usedPageAttribute,
+ }
+ );
+ });
+ });
+ },
+
+ async check() {
+ // Check that used attributes are still there
+ let value = await PlacesTestUtils.getDatabaseValue(
+ "moz_anno_attributes",
+ "id",
+ {
+ name: this._usedPageAttribute,
+ }
+ );
+ Assert.notStrictEqual(value, undefined);
+ // Check that unused attribute has been removed
+ value = await PlacesTestUtils.getDatabaseValue(
+ "moz_anno_attributes",
+ "id",
+ {
+ name: this._unusedAttribute,
+ }
+ );
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "B.1",
+ desc: "Remove annotations with an invalid attribute",
+
+ _usedPageAttribute: "usedPage",
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeTransaction(async () => {
+ // Add a used attribute.
+ await db.executeCached(
+ "INSERT INTO moz_anno_attributes (name) VALUES (:anno)",
+ { anno: this._usedPageAttribute }
+ );
+ await db.executeCached(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))`,
+ {
+ place_id: this._placeId,
+ anno: this._usedPageAttribute,
+ }
+ );
+ // Add an annotation with a nonexistent attribute
+ await db.executeCached(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES(:place_id, 1337)`,
+ { place_id: this._placeId }
+ );
+ });
+ });
+ },
+
+ async check() {
+ // Check that used attribute is still there
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ "SELECT id FROM moz_anno_attributes WHERE name = :anno",
+ { anno: this._usedPageAttribute }
+ );
+ Assert.equal(rows.length, 1);
+ // check that annotation with valid attribute is still there
+ rows = await db.executeCached(
+ `SELECT id FROM moz_annos
+ WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)`,
+ { anno: this._usedPageAttribute }
+ );
+ Assert.equal(rows.length, 1);
+ // Check that annotation with bogus attribute has been removed
+ let value = await PlacesTestUtils.getDatabaseValue("moz_annos", "id", {
+ anno_attribute_id: 1337,
+ });
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "B.2",
+ desc: "Remove orphan page annotations",
+
+ _usedPageAttribute: "usedPage",
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeTransaction(async () => {
+ // Add a used attribute.
+ await db.executeCached(
+ "INSERT INTO moz_anno_attributes (name) VALUES (:anno)",
+ { anno: this._usedPageAttribute }
+ );
+ await db.executeCached(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))`,
+ { place_id: this._placeId, anno: this._usedPageAttribute }
+ );
+ // Add an annotation to a nonexistent page
+ await db.executeCached(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))`,
+ { place_id: 1337, anno: this._usedPageAttribute }
+ );
+ });
+ });
+ },
+
+ async check() {
+ // Check that used attribute is still there
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ "SELECT id FROM moz_anno_attributes WHERE name = :anno",
+ { anno: this._usedPageAttribute }
+ );
+ Assert.equal(rows.length, 1);
+ // check that annotation with valid attribute is still there
+ rows = await db.executeCached(
+ `SELECT id FROM moz_annos
+ WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)`,
+ { anno: this._usedPageAttribute }
+ );
+ Assert.equal(rows.length, 1);
+ // Check that an annotation to a nonexistent page has been removed
+ let value = await PlacesTestUtils.getDatabaseValue("moz_annos", "id", {
+ place_id: 1337,
+ });
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.9",
+ desc: "Remove items without a valid place",
+
+ _validItemId: null,
+ _invalidItemId: null,
+ _invalidSyncedItemId: null,
+ placeId: null,
+
+ _changeCounterStmt: null,
+ _menuChangeCounter: -1,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this.placeId = await addPlace();
+ // Insert a valid bookmark
+ this._validItemId = await addBookmark(
+ this.placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ );
+ // Insert a bookmark with an invalid place
+ this._invalidItemId = await addBookmark(
+ 1337,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ );
+ // Insert a synced bookmark with an invalid place. We should write a
+ // tombstone when we remove it.
+ this._invalidSyncedItemId = await addBookmark(
+ 1337,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ PlacesUtils.bookmarks.menuGuid,
+ null,
+ null,
+ "bookmarkAAAA",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+ // Insert a folder referencing a nonexistent place ID. D.5 should convert
+ // it to a bookmark; D.9 should remove it.
+ this._invalidWrongTypeItemId = await addBookmark(
+ 1337,
+ PlacesUtils.bookmarks.TYPE_FOLDER
+ );
+
+ let value = await PlacesTestUtils.getDatabaseValue(
+ "moz_bookmarks",
+ "syncChangeCounter",
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ }
+ );
+ Assert.equal(value, 0);
+ this._menuChangeCounter = value;
+ },
+
+ async check() {
+ // Check that valid bookmark is still there
+ let value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "id", {
+ id: this._validItemId,
+ });
+ Assert.notStrictEqual(value, undefined);
+ // Check that invalid bookmarks have been removed
+ for (let id of [
+ this._invalidItemId,
+ this._invalidSyncedItemId,
+ this._invalidWrongTypeItemId,
+ ]) {
+ value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "id", {
+ id,
+ });
+ Assert.strictEqual(value, undefined);
+ }
+
+ value = await PlacesTestUtils.getDatabaseValue(
+ "moz_bookmarks",
+ "syncChangeCounter",
+ { guid: PlacesUtils.bookmarks.menuGuid }
+ );
+ Assert.equal(value, 1);
+ Assert.equal(value, this._menuChangeCounter + 1);
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.deepEqual(
+ tombstones.map(info => info.guid),
+ ["bookmarkAAAA"]
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.1",
+ desc: "Remove items that are not uri bookmarks from tag containers",
+
+ _tagId: null,
+ _bookmarkId: null,
+ _separatorId: null,
+ _folderId: null,
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Create a tag
+ this._tagId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.tagsGuid
+ );
+ let tagGuid = await PlacesUtils.promiseItemGuid(this._tagId);
+ // Insert a bookmark in the tag
+ this._bookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ tagGuid
+ );
+ // Insert a separator in the tag
+ this._separatorId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ tagGuid
+ );
+ // Insert a folder in the tag
+ this._folderId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ tagGuid
+ );
+ },
+
+ async check() {
+ // Check that valid bookmark is still there
+ let value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "id", {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parent: this._tagId,
+ });
+ Assert.notStrictEqual(value, undefined);
+ // Check that separator is no more there
+ value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "id", {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parent: this._tagId,
+ });
+ Assert.equal(value, undefined);
+ // Check that folder is no more there
+ value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "id", {
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parent: this._tagId,
+ });
+ Assert.equal(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.2",
+ desc: "Remove empty tags",
+
+ _tagId: null,
+ _bookmarkId: null,
+ _emptyTagId: null,
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Create a tag
+ this._tagId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.tagsGuid
+ );
+ let tagGuid = await PlacesUtils.promiseItemGuid(this._tagId);
+ // Insert a bookmark in the tag
+ this._bookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ tagGuid
+ );
+ // Create another tag (empty)
+ this._emptyTagId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.tagsGuid
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ // Check that valid bookmark is still there
+ let value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "id", {
+ id: this._bookmarkId,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parent: this._tagId,
+ });
+ Assert.notStrictEqual(value, undefined);
+ let rows = await db.executeCached(
+ `SELECT b.id FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON p.id = b.parent
+ WHERE b.id = :id AND b.type = :type AND p.guid = :parent`,
+ {
+ id: this._tagId,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parent: PlacesUtils.bookmarks.tagsGuid,
+ }
+ );
+ Assert.equal(rows.length, 1);
+ rows = await db.executeCached(
+ `SELECT b.id FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON p.id = b.parent
+ WHERE b.id = :id AND b.type = :type AND p.guid = :parent`,
+ {
+ id: this._emptyTagId,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parent: PlacesUtils.bookmarks.tagsGuid,
+ }
+ );
+ Assert.equal(rows.length, 0);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.3",
+ desc: "Move orphan items to unsorted folder",
+
+ _orphanBookmarkId: null,
+ _orphanSeparatorId: null,
+ _orphanFolderId: null,
+ _bookmarkId: null,
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Insert an orphan bookmark
+ this._orphanBookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ 8888
+ );
+ // Insert an orphan separator
+ this._orphanSeparatorId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ 8888
+ );
+ // Insert a orphan folder
+ this._orphanFolderId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ 8888
+ );
+ let folderGuid = await PlacesUtils.promiseItemGuid(this._orphanFolderId);
+ // Create a child of the last created folder
+ this._bookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ folderGuid
+ );
+ },
+
+ async check() {
+ // Check that bookmarks are now children of a real folder (unfiled)
+ let expectedInfos = [
+ {
+ id: this._orphanBookmarkId,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._orphanSeparatorId,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._orphanFolderId,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._bookmarkId,
+ parent: await PlacesUtils.promiseItemGuid(this._orphanFolderId),
+ syncChangeCounter: 0,
+ },
+ {
+ id: await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid),
+ parent: PlacesUtils.bookmarks.rootGuid,
+ syncChangeCounter: 3,
+ },
+ ];
+ let db = await PlacesUtils.promiseDBConnection();
+ for (let { id, parent, syncChangeCounter } of expectedInfos) {
+ let rows = await db.executeCached(
+ `
+ SELECT b.id, b.syncChangeCounter
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE b.id = :item_id
+ AND p.guid = :parent`,
+ { item_id: id, parent }
+ );
+ Assert.equal(rows.length, 1);
+
+ let actualChangeCounter = rows[0].getResultByName("syncChangeCounter");
+ Assert.equal(actualChangeCounter, syncChangeCounter);
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.5",
+ desc: "Fix wrong item types | folders and separators",
+
+ _separatorId: null,
+ _separatorGuid: null,
+ _folderId: null,
+ _folderGuid: null,
+ _syncedFolderId: null,
+ _syncedFolderGuid: null,
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Add a separator with a fk
+ this._separatorId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_SEPARATOR
+ );
+ this._separatorGuid = await PlacesUtils.promiseItemGuid(this._separatorId);
+ // Add a folder with a fk
+ this._folderId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_FOLDER
+ );
+ this._folderGuid = await PlacesUtils.promiseItemGuid(this._folderId);
+ // Add a synced folder with a fk
+ this._syncedFolderId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ null,
+ "itemAAAAAAAA",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+ this._syncedFolderGuid = await PlacesUtils.promiseItemGuid(
+ this._syncedFolderId
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ // Check that items with an fk have been converted to bookmarks
+ let rows = await db.executeCached(
+ `SELECT id, guid, syncChangeCounter
+ FROM moz_bookmarks
+ WHERE id = :item_id AND type = :type`,
+ { item_id: this._separatorId, type: PlacesUtils.bookmarks.TYPE_BOOKMARK }
+ );
+ Assert.equal(rows.length, 1);
+ let expected = [
+ {
+ id: this._folderId,
+ oldGuid: this._folderGuid,
+ },
+ {
+ id: this._syncedFolderId,
+ oldGuid: this._syncedFolderGuid,
+ },
+ ];
+ for (let { id, oldGuid } of expected) {
+ rows = await db.executeCached(
+ `SELECT id, guid, syncChangeCounter
+ FROM moz_bookmarks
+ WHERE id = :item_id AND type = :type`,
+ { item_id: id, type: PlacesUtils.bookmarks.TYPE_BOOKMARK }
+ );
+ Assert.equal(rows.length, 1);
+ Assert.notEqual(rows[0].getResultByName("guid"), oldGuid);
+ Assert.equal(rows[0].getResultByName("syncChangeCounter"), 1);
+ await Assert.rejects(
+ PlacesUtils.promiseItemId(oldGuid),
+ /no item found for the given GUID/
+ );
+ }
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.deepEqual(
+ tombstones.map(info => info.guid),
+ ["itemAAAAAAAA"]
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.6",
+ desc: "Fix wrong item types | bookmarks",
+
+ _validBookmarkId: null,
+ _validBookmarkGuid: null,
+ _invalidBookmarkId: null,
+ _invalidBookmarkGuid: null,
+ _invalidSyncedBookmarkId: null,
+ _invalidSyncedBookmarkGuid: null,
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Add a bookmark with a valid place id
+ this._validBookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ );
+ this._validBookmarkGuid = await PlacesUtils.promiseItemGuid(
+ this._validBookmarkId
+ );
+ // Add a bookmark with a null place id
+ this._invalidBookmarkId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ );
+ this._invalidBookmarkGuid = await PlacesUtils.promiseItemGuid(
+ this._invalidBookmarkId
+ );
+ // Add a synced bookmark with a null place id
+ this._invalidSyncedBookmarkId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ null,
+ "bookmarkAAAA",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+ this._invalidSyncedBookmarkGuid = await PlacesUtils.promiseItemGuid(
+ this._invalidSyncedBookmarkId
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ // Check valid bookmark
+ let rows = await db.executeCached(
+ `SELECT id, guid, syncChangeCounter
+ FROM moz_bookmarks
+ WHERE id = :item_id AND type = :type`,
+ {
+ item_id: this._validBookmarkId,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ }
+ );
+ Assert.equal(rows.length, 1);
+ Assert.equal(rows[0].getResultByName("syncChangeCounter"), 0);
+ Assert.equal(
+ await PlacesUtils.promiseItemId(this._validBookmarkGuid),
+ this._validBookmarkId
+ );
+
+ // Check invalid bookmarks have been converted to folders
+ let expected = [
+ {
+ id: this._invalidBookmarkId,
+ oldGuid: this._invalidBookmarkGuid,
+ },
+ {
+ id: this._invalidSyncedBookmarkId,
+ oldGuid: this._invalidSyncedBookmarkGuid,
+ },
+ ];
+ for (let { id, oldGuid } of expected) {
+ rows = await db.executeCached(
+ `SELECT id, guid, syncChangeCounter
+ FROM moz_bookmarks
+ WHERE id = :item_id AND type = :type`,
+ { item_id: id, type: PlacesUtils.bookmarks.TYPE_FOLDER }
+ );
+ Assert.equal(rows.length, 1);
+ Assert.notEqual(rows[0].getResultByName("guid"), oldGuid);
+ Assert.equal(rows[0].getResultByName("syncChangeCounter"), 1);
+ await Assert.rejects(
+ PlacesUtils.promiseItemId(oldGuid),
+ /no item found for the given GUID/
+ );
+ }
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.deepEqual(
+ tombstones.map(info => info.guid),
+ ["bookmarkAAAA"]
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.7",
+ desc: "Fix missing item types",
+
+ _placeId: null,
+ _bookmarkId: null,
+ _bookmarkGuid: null,
+ _syncedBookmarkId: null,
+ _syncedBookmarkGuid: null,
+ _folderId: null,
+ _folderGuid: null,
+ _syncedFolderId: null,
+ _syncedFolderGuid: null,
+
+ async setup() {
+ // Item without a type but with a place ID; should be converted to a
+ // bookmark. The synced bookmark should be handled the same way, but with
+ // a tombstone.
+ this._placeId = await addPlace();
+ this._bookmarkId = await addBookmark(this._placeId);
+ this._bookmarkGuid = await PlacesUtils.promiseItemGuid(this._bookmarkId);
+ this._syncedBookmarkId = await addBookmark(
+ this._placeId,
+ null,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ null,
+ "bookmarkAAAA",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+ this._syncedBookmarkGuid = await PlacesUtils.promiseItemGuid(
+ this._syncedBookmarkId
+ );
+
+ // Item without a type and without a place ID; should be converted to a
+ // folder.
+ this._folderId = await addBookmark();
+ this._folderGuid = await PlacesUtils.promiseItemGuid(this._folderId);
+ this._syncedFolderId = await addBookmark(
+ null,
+ null,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ null,
+ "folderBBBBBB",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+ this._syncedFolderGuid = await PlacesUtils.promiseItemGuid(
+ this._syncedFolderId
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let expected = [
+ {
+ id: this._bookmarkId,
+ oldGuid: this._bookmarkGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._syncedBookmarkId,
+ oldGuid: this._syncedBookmarkGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._folderId,
+ oldGuid: this._folderGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._syncedFolderId,
+ oldGuid: this._syncedFolderGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ syncChangeCounter: 1,
+ },
+ ];
+ for (let { id, oldGuid, type, syncChangeCounter } of expected) {
+ let rows = await db.executeCached(
+ `SELECT id, guid, type, syncChangeCounter
+ FROM moz_bookmarks
+ WHERE id = :item_id`,
+ { item_id: id }
+ );
+ Assert.equal(rows.length, 1);
+ Assert.equal(rows[0].getResultByName("type"), type);
+ Assert.equal(
+ rows[0].getResultByName("syncChangeCounter"),
+ syncChangeCounter
+ );
+ Assert.notEqual(rows[0].getResultByName("guid"), oldGuid);
+ await Assert.rejects(
+ PlacesUtils.promiseItemId(oldGuid),
+ /no item found for the given GUID/
+ );
+ }
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.deepEqual(
+ tombstones.map(info => info.guid),
+ ["bookmarkAAAA", "folderBBBBBB"]
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.8",
+ desc: "Fix wrong parents",
+
+ _bookmarkId: null,
+ _separatorId: null,
+ _bookmarkId1: null,
+ _bookmarkId2: null,
+ _placeId: null,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Insert a bookmark
+ this._bookmarkId = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ );
+ // Insert a separator
+ this._separatorId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_SEPARATOR
+ );
+ // Create 3 children of these items
+ let bookmarkGuid = await PlacesUtils.promiseItemGuid(this._bookmarkId);
+ this._bookmarkId1 = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ bookmarkGuid
+ );
+ let separatorGuid = await PlacesUtils.promiseItemGuid(this._separatorId);
+ this._bookmarkId2 = await addBookmark(
+ this._placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ separatorGuid
+ );
+ },
+
+ async check() {
+ // Check that bookmarks are now children of a real folder (unfiled)
+ let expectedInfos = [
+ {
+ id: this._bookmarkId1,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ syncChangeCounter: 1,
+ },
+ {
+ id: this._bookmarkId2,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ syncChangeCounter: 1,
+ },
+ {
+ id: await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid),
+ parent: PlacesUtils.bookmarks.rootGuid,
+ syncChangeCounter: 2,
+ },
+ ];
+ let db = await PlacesUtils.promiseDBConnection();
+ for (let { id, parent, syncChangeCounter } of expectedInfos) {
+ let rows = await db.executeCached(
+ `
+ SELECT b.id, b.syncChangeCounter
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE b.id = :item_id AND p.guid = :parent`,
+ { item_id: id, parent }
+ );
+ Assert.equal(rows.length, 1);
+
+ let actualChangeCounter = rows[0].getResultByName("syncChangeCounter");
+ Assert.equal(actualChangeCounter, syncChangeCounter);
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.10",
+ desc: "Recalculate positions",
+
+ _unfiledBookmarks: [],
+ _toolbarBookmarks: [],
+
+ async setup() {
+ const NUM_BOOKMARKS = 20;
+ let children = [];
+ for (let i = 0; i < NUM_BOOKMARKS; i++) {
+ children.push({
+ title: "testbookmark",
+ url: "http://example.com",
+ });
+ }
+
+ // Add bookmarks to two folders to better perturbate the table.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ });
+
+ async function randomize_positions(aParent, aResultArray) {
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeTransaction(async () => {
+ for (let i = 0; i < NUM_BOOKMARKS / 2; i++) {
+ await db.executeCached(
+ `UPDATE moz_bookmarks SET position = :rand
+ WHERE id IN (
+ SELECT b.id FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE p.guid = :parent
+ ORDER BY RANDOM() LIMIT 1
+ )`,
+ {
+ parent: aParent,
+ rand: Math.round(Math.random() * (NUM_BOOKMARKS - 1)),
+ }
+ );
+ }
+
+ // Build the expected ordered list of bookmarks.
+ let rows = await db.executeCached(
+ `SELECT b.id
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE p.guid = :parent
+ ORDER BY b.position ASC, b.ROWID ASC`,
+ { parent: aParent }
+ );
+ rows.forEach(r => {
+ aResultArray.push(r.getResultByName("id"));
+ });
+ await PlacesTestUtils.dumpTable({
+ db,
+ table: "moz_bookmarks",
+ columns: ["id", "parent", "position"],
+ });
+ });
+ });
+ }
+
+ // Set random positions for the added bookmarks.
+ await randomize_positions(
+ PlacesUtils.bookmarks.unfiledGuid,
+ this._unfiledBookmarks
+ );
+ await randomize_positions(
+ PlacesUtils.bookmarks.toolbarGuid,
+ this._toolbarBookmarks
+ );
+
+ let syncInfos = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ Assert.ok(syncInfos.every(info => info.syncChangeCounter === 0));
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+
+ async function check_order(aParent, aResultArray) {
+ // Build the expected ordered list of bookmarks.
+ let childRows = await db.executeCached(
+ `SELECT b.id, b.position, b.syncChangeCounter
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE p.guid = :parent
+ ORDER BY b.position ASC`,
+ { parent: aParent }
+ );
+ for (let row of childRows) {
+ let id = row.getResultByName("id");
+ let position = row.getResultByName("position");
+ if (aResultArray.indexOf(id) != position) {
+ info("Expected order: " + aResultArray);
+ await PlacesTestUtils.dumpTable({
+ db,
+ table: "moz_bookmarks",
+ columns: ["id", "parent", "position"],
+ });
+ do_throw(`Unexpected bookmarks order for ${aParent}.`);
+ }
+ }
+
+ let parentRows = await db.executeCached(
+ `SELECT syncChangeCounter FROM moz_bookmarks
+ WHERE guid = :parent`,
+ { parent: aParent }
+ );
+ for (let row of parentRows) {
+ let actualChangeCounter = row.getResultByName("syncChangeCounter");
+ Assert.ok(actualChangeCounter > 0);
+ }
+ }
+
+ await check_order(
+ PlacesUtils.bookmarks.unfiledGuid,
+ this._unfiledBookmarks
+ );
+ await check_order(
+ PlacesUtils.bookmarks.toolbarGuid,
+ this._toolbarBookmarks
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.13",
+ desc: "Fix empty-named tags",
+ _taggedItemIds: {},
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ let placeId = await addPlace();
+ // Create a empty-named tag.
+ this._untitledTagId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.tagsGuid,
+ null,
+ ""
+ );
+ // Insert a bookmark in the tag, otherwise it will be removed.
+ let untitledTagGuid = await PlacesUtils.promiseItemGuid(
+ this._untitledTagId
+ );
+ await addBookmark(
+ placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ untitledTagGuid
+ );
+ // Create a empty-named folder.
+ this._untitledFolderId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ ""
+ );
+ // Create a titled tag.
+ this._titledTagId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.tagsGuid,
+ null,
+ "titledTag"
+ );
+ // Insert a bookmark in the tag, otherwise it will be removed.
+ let titledTagGuid = await PlacesUtils.promiseItemGuid(this._titledTagId);
+ await addBookmark(
+ placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ titledTagGuid
+ );
+ // Create a titled folder.
+ this._titledFolderId = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ "titledFolder"
+ );
+
+ // Create two tagged bookmarks in different folders.
+ this._taggedItemIds.inMenu = await addBookmark(
+ placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ PlacesUtils.bookmarks.menuGuid,
+ null,
+ "Tagged bookmark in menu"
+ );
+ this._taggedItemIds.inToolbar = await addBookmark(
+ placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ PlacesUtils.bookmarks.toolbarGuid,
+ null,
+ "Tagged bookmark in toolbar"
+ );
+ },
+
+ async check() {
+ // Check that valid bookmark is still there
+ let value = await PlacesTestUtils.getDatabaseValue(
+ "moz_bookmarks",
+ "title",
+ { id: this._untitledTagId }
+ );
+ Assert.equal(value, "(notitle)");
+ value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "title", {
+ id: this._untitledFolderId,
+ });
+ Assert.equal(value, "");
+ value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "title", {
+ id: this._titledTagId,
+ });
+ Assert.equal(value, "titledTag");
+ value = await PlacesTestUtils.getDatabaseValue("moz_bookmarks", "title", {
+ id: this._titledFolderId,
+ });
+ Assert.equal(value, "titledFolder");
+
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ `SELECT syncChangeCounter FROM moz_bookmarks
+ WHERE id IN (:taggedInMenu, :taggedInToolbar)`,
+ {
+ taggedInMenu: this._taggedItemIds.inMenu,
+ taggedInToolbar: this._taggedItemIds.inToolbar,
+ }
+ );
+ for (let row of rows) {
+ Assert.greaterOrEqual(row.getResultByName("syncChangeCounter"), 1);
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "E.1",
+ desc: "Remove orphan icon entries",
+
+ _placeId: null,
+
+ async setup() {
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeTransaction(async () => {
+ // Insert favicon entries
+ await db.executeCached(
+ `INSERT INTO moz_icons (id, icon_url, fixed_icon_url_hash, root) VALUES(:favicon_id, :url, hash(fixup_url(:url)), :root)`,
+ [
+ {
+ favicon_id: 1,
+ url: "http://www1.mozilla.org/favicon.ico",
+ root: 0,
+ },
+ {
+ favicon_id: 2,
+ url: "http://www2.mozilla.org/favicon.ico",
+ root: 0,
+ },
+ {
+ favicon_id: 3,
+ url: "http://www3.mozilla.org/favicon.ico",
+ root: 1,
+ },
+ ]
+ );
+
+ // Insert orphan page.
+ await db.executeCached(
+ `INSERT INTO moz_pages_w_icons (id, page_url, page_url_hash)
+ VALUES(:page_id, :url, hash(:url))`,
+ { page_id: 99, url: "http://w99.mozilla.org/" }
+ );
+ });
+ });
+
+ // Insert a place using the existing favicon entry
+ this._placeId = await addPlace("http://www.mozilla.org", 1);
+ },
+
+ async check() {
+ // Check that used icon is still there
+ let value = await PlacesTestUtils.getDatabaseValue("moz_icons", "id", {
+ id: 1,
+ });
+ Assert.notStrictEqual(value, undefined);
+ // Check that unused icon has been removed
+ value = await PlacesTestUtils.getDatabaseValue("moz_icons", "id", {
+ id: 2,
+ });
+ Assert.strictEqual(value, undefined);
+ // Check that unused icon has been removed
+ value = await PlacesTestUtils.getDatabaseValue("moz_icons", "id", {
+ id: 3,
+ });
+ Assert.strictEqual(value, undefined);
+ // Check that the orphan page is gone.
+ value = await PlacesTestUtils.getDatabaseValue("moz_pages_w_icons", "id", {
+ id: 99,
+ });
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "F.1",
+ desc: "Remove orphan visits",
+
+ _placeId: null,
+ _invalidPlaceId: 1337,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Add a valid visit and an invalid one
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeCached(
+ `INSERT INTO moz_historyvisits(place_id)
+ VALUES (:place_id_1), (:place_id_2)`,
+ { place_id_1: this._placeId, place_id_2: this._invalidPlaceId }
+ );
+ });
+ },
+
+ async check() {
+ // Check that valid visit is still there
+ let value = await PlacesTestUtils.getDatabaseValue(
+ "moz_historyvisits",
+ "id",
+ {
+ place_id: this._placeId,
+ }
+ );
+ Assert.notStrictEqual(value, undefined);
+ // Check that invalid visit has been removed
+ value = await PlacesTestUtils.getDatabaseValue("moz_historyvisits", "id", {
+ place_id: this._invalidPlaceId,
+ });
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "G.1",
+ desc: "Remove orphan input history",
+
+ _placeId: null,
+ _invalidPlaceId: 1337,
+
+ async setup() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = await addPlace();
+ // Add input history entries
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeCached(
+ `INSERT INTO moz_inputhistory (place_id, input)
+ VALUES (:place_id_1, :input_1), (:place_id_2, :input_2)`,
+ {
+ place_id_1: this._placeId,
+ input_1: "moz",
+ place_id_2: this._invalidPlaceId,
+ input_2: "moz",
+ }
+ );
+ });
+ },
+
+ async check() {
+ // Check that inputhistory on valid place is still there
+ let value = await PlacesTestUtils.getDatabaseValue(
+ "moz_inputhistory",
+ "place_id",
+ { place_id: this._placeId }
+ );
+ Assert.notStrictEqual(value, undefined);
+ // Check that inputhistory on invalid place has gone
+ value = await PlacesTestUtils.getDatabaseValue(
+ "moz_inputhistory",
+ "place_id",
+ { place_id: this._invalidPlaceId }
+ );
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "I.1",
+ desc: "Remove unused keywords",
+
+ _bookmarkId: null,
+ _placeId: null,
+
+ async setup() {
+ // Insert 2 keywords
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: "http://testkw.moz.org/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.keywords.insert({
+ url: bm.url,
+ keyword: "used",
+ });
+
+ await PlacesUtils.withConnectionWrapper("setup", async db => {
+ await db.executeCached(
+ `INSERT INTO moz_keywords (id, keyword, place_id)
+ VALUES(NULL, :keyword, :place_id)`,
+ { keyword: "unused", place_id: 100 }
+ );
+ });
+ },
+
+ async check() {
+ // Check that "used" keyword is still there
+ let value = await PlacesTestUtils.getDatabaseValue("moz_keywords", "id", {
+ keyword: "used",
+ });
+ Assert.notStrictEqual(value, undefined);
+ // Check that "unused" keyword has gone
+ value = await PlacesTestUtils.getDatabaseValue("moz_keywords", "id", {
+ keyword: "unused",
+ });
+ Assert.strictEqual(value, undefined);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.1",
+ desc: "remove duplicate URLs",
+ _placeA: -1,
+ _placeD: -1,
+ _placeE: -1,
+ _bookmarkIds: [],
+
+ async setup() {
+ // Place with visits, an autocomplete history entry, anno, and a bookmark.
+ this._placeA = await addPlace("http://example.com", null, "placeAAAAAAA");
+
+ // Duplicate Place with different visits and a keyword.
+ let placeB = await addPlace("http://example.com", null, "placeBBBBBBB");
+
+ // Another duplicate with conflicting autocomplete history entry and
+ // two more bookmarks.
+ let placeC = await addPlace("http://example.com", null, "placeCCCCCCC");
+
+ // Unrelated, unique Place.
+ this._placeD = await addPlace(
+ "http://example.net",
+ null,
+ "placeDDDDDDD",
+ 1234
+ );
+
+ // Another unrelated Place, with the same hash as D, but different URL.
+ this._placeE = await addPlace(
+ "http://example.info",
+ null,
+ "placeEEEEEEE",
+ 1234
+ );
+
+ let visits = [
+ {
+ placeId: this._placeA,
+ date: new Date(2017, 1, 2),
+ type: PlacesUtils.history.TRANSITIONS.LINK,
+ },
+ {
+ placeId: this._placeA,
+ date: new Date(2018, 3, 4),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ placeId: placeB,
+ date: new Date(2016, 5, 6),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ // Duplicate visit; should keep both when we merge.
+ placeId: placeB,
+ date: new Date(2018, 3, 4),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ placeId: this._placeD,
+ date: new Date(2018, 7, 8),
+ type: PlacesUtils.history.TRANSITIONS.LINK,
+ },
+ {
+ placeId: this._placeE,
+ date: new Date(2018, 8, 9),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ];
+
+ let inputs = [
+ {
+ placeId: this._placeA,
+ input: "exam",
+ count: 4,
+ },
+ {
+ placeId: placeC,
+ input: "exam",
+ count: 3,
+ },
+ {
+ placeId: placeC,
+ input: "ex",
+ count: 5,
+ },
+ {
+ placeId: this._placeD,
+ input: "amp",
+ count: 3,
+ },
+ ];
+
+ let annos = [
+ {
+ name: "anno",
+ placeId: this._placeA,
+ content: "splish",
+ },
+ {
+ // Anno that's already set on A; should be ignored when we merge.
+ name: "anno",
+ placeId: placeB,
+ content: "oops",
+ },
+ {
+ name: "other-anno",
+ placeId: placeB,
+ content: "splash",
+ },
+ {
+ name: "other-anno",
+ placeId: this._placeD,
+ content: "sploosh",
+ },
+ ];
+
+ let bookmarks = [
+ {
+ placeId: this._placeA,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "A",
+ guid: "bookmarkAAAA",
+ },
+ {
+ placeId: placeB,
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ title: "B",
+ guid: "bookmarkBBBB",
+ },
+ {
+ placeId: placeC,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "C1",
+ guid: "bookmarkCCC1",
+ },
+ {
+ placeId: placeC,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "C2",
+ guid: "bookmarkCCC2",
+ },
+ {
+ placeId: this._placeD,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "D",
+ guid: "bookmarkDDDD",
+ },
+ {
+ placeId: this._placeE,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "E",
+ guid: "bookmarkEEEE",
+ },
+ ];
+
+ let keywords = [
+ {
+ placeId: placeB,
+ keyword: "hi",
+ },
+ {
+ placeId: this._placeD,
+ keyword: "bye",
+ },
+ ];
+
+ for (let { placeId, parentGuid, title, guid } of bookmarks) {
+ let itemId = await addBookmark(
+ placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid,
+ null,
+ title,
+ guid
+ );
+ this._bookmarkIds.push(itemId);
+ }
+
+ await PlacesUtils.withConnectionWrapper(
+ "L.1: Insert foreign key refs",
+ function (db) {
+ return db.executeTransaction(async function () {
+ for (let { placeId, date, type } of visits) {
+ await db.executeCached(
+ `INSERT INTO moz_historyvisits(place_id, visit_date, visit_type)
+ VALUES(:placeId, :date, :type)`,
+ { placeId, date: PlacesUtils.toPRTime(date), type }
+ );
+ }
+
+ for (let params of inputs) {
+ await db.executeCached(
+ `INSERT INTO moz_inputhistory(place_id, input, use_count)
+ VALUES(:placeId, :input, :count)`,
+ params
+ );
+ }
+
+ for (let { name, placeId, content } of annos) {
+ await db.executeCached(
+ `INSERT OR IGNORE INTO moz_anno_attributes(name)
+ VALUES(:name)`,
+ { name }
+ );
+
+ await db.executeCached(
+ `INSERT INTO moz_annos(place_id, anno_attribute_id, content)
+ VALUES(:placeId, (SELECT id FROM moz_anno_attributes
+ WHERE name = :name), :content)`,
+ { placeId, name, content }
+ );
+ }
+
+ for (let param of keywords) {
+ await db.executeCached(
+ `INSERT INTO moz_keywords(keyword, place_id)
+ VALUES(:keyword, :placeId)`,
+ param
+ );
+ }
+ });
+ }
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+
+ let placeRows = await db.execute(`
+ SELECT id, guid, foreign_count FROM moz_places
+ ORDER BY guid`);
+ let placeInfos = placeRows.map(row => ({
+ id: row.getResultByName("id"),
+ guid: row.getResultByName("guid"),
+ foreignCount: row.getResultByName("foreign_count"),
+ }));
+ Assert.deepEqual(
+ placeInfos,
+ [
+ {
+ id: this._placeA,
+ guid: "placeAAAAAAA",
+ foreignCount: 5, // 4 bookmarks + 1 keyword
+ },
+ {
+ id: this._placeD,
+ guid: "placeDDDDDDD",
+ foreignCount: 2, // 1 bookmark + 1 keyword
+ },
+ {
+ id: this._placeE,
+ guid: "placeEEEEEEE",
+ foreignCount: 1, // 1 bookmark
+ },
+ ],
+ "Should remove duplicate Places B and C"
+ );
+
+ let visitRows = await db.execute(`
+ SELECT place_id, visit_date, visit_type FROM moz_historyvisits
+ ORDER BY visit_date`);
+ let visitInfos = visitRows.map(row => ({
+ placeId: row.getResultByName("place_id"),
+ date: PlacesUtils.toDate(row.getResultByName("visit_date")),
+ type: row.getResultByName("visit_type"),
+ }));
+ Assert.deepEqual(
+ visitInfos,
+ [
+ {
+ placeId: this._placeA,
+ date: new Date(2016, 5, 6),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ placeId: this._placeA,
+ date: new Date(2017, 1, 2),
+ type: PlacesUtils.history.TRANSITIONS.LINK,
+ },
+ {
+ placeId: this._placeA,
+ date: new Date(2018, 3, 4),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ placeId: this._placeA,
+ date: new Date(2018, 3, 4),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ {
+ placeId: this._placeD,
+ date: new Date(2018, 7, 8),
+ type: PlacesUtils.history.TRANSITIONS.LINK,
+ },
+ {
+ placeId: this._placeE,
+ date: new Date(2018, 8, 9),
+ type: PlacesUtils.history.TRANSITIONS.TYPED,
+ },
+ ],
+ "Should merge history visits"
+ );
+
+ let inputRows = await db.execute(`
+ SELECT place_id, input, use_count FROM moz_inputhistory
+ ORDER BY use_count ASC`);
+ let inputInfos = inputRows.map(row => ({
+ placeId: row.getResultByName("place_id"),
+ input: row.getResultByName("input"),
+ count: row.getResultByName("use_count"),
+ }));
+ Assert.deepEqual(
+ inputInfos,
+ [
+ {
+ placeId: this._placeD,
+ input: "amp",
+ count: 3,
+ },
+ {
+ placeId: this._placeA,
+ input: "ex",
+ count: 5,
+ },
+ {
+ placeId: this._placeA,
+ input: "exam",
+ count: 7,
+ },
+ ],
+ "Should merge autocomplete history"
+ );
+
+ let annoRows = await db.execute(`
+ SELECT a.place_id, n.name, a.content FROM moz_annos a
+ JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
+ ORDER BY n.name, a.content ASC`);
+ let annoInfos = annoRows.map(row => ({
+ placeId: row.getResultByName("place_id"),
+ name: row.getResultByName("name"),
+ content: row.getResultByName("content"),
+ }));
+ Assert.deepEqual(
+ annoInfos,
+ [
+ {
+ placeId: this._placeA,
+ name: "anno",
+ content: "splish",
+ },
+ {
+ placeId: this._placeA,
+ name: "other-anno",
+ content: "splash",
+ },
+ {
+ placeId: this._placeD,
+ name: "other-anno",
+ content: "sploosh",
+ },
+ ],
+ "Should merge page annos"
+ );
+
+ let itemRows = await db.execute(
+ `
+ SELECT guid, fk, syncChangeCounter FROM moz_bookmarks
+ WHERE id IN (${new Array(this._bookmarkIds.length).fill("?").join(",")})
+ ORDER BY guid ASC`,
+ this._bookmarkIds
+ );
+ let itemInfos = itemRows.map(row => ({
+ guid: row.getResultByName("guid"),
+ placeId: row.getResultByName("fk"),
+ syncChangeCounter: row.getResultByName("syncChangeCounter"),
+ }));
+ Assert.deepEqual(
+ itemInfos,
+ [
+ {
+ guid: "bookmarkAAAA",
+ placeId: this._placeA,
+ syncChangeCounter: 1,
+ },
+ {
+ guid: "bookmarkBBBB",
+ placeId: this._placeA,
+ syncChangeCounter: 1,
+ },
+ {
+ guid: "bookmarkCCC1",
+ placeId: this._placeA,
+ syncChangeCounter: 1,
+ },
+ {
+ guid: "bookmarkCCC2",
+ placeId: this._placeA,
+ syncChangeCounter: 1,
+ },
+ {
+ guid: "bookmarkDDDD",
+ placeId: this._placeD,
+ syncChangeCounter: 0,
+ },
+ {
+ guid: "bookmarkEEEE",
+ placeId: this._placeE,
+ syncChangeCounter: 0,
+ },
+ ],
+ "Should merge bookmarks and bump change counter"
+ );
+
+ let keywordRows = await db.execute(`
+ SELECT keyword, place_id FROM moz_keywords
+ ORDER BY keyword ASC`);
+ let keywordInfos = keywordRows.map(row => ({
+ keyword: row.getResultByName("keyword"),
+ placeId: row.getResultByName("place_id"),
+ }));
+ Assert.deepEqual(
+ keywordInfos,
+ [
+ {
+ keyword: "bye",
+ placeId: this._placeD,
+ },
+ {
+ keyword: "hi",
+ placeId: this._placeA,
+ },
+ ],
+ "Should merge all keywords"
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.2",
+ desc: "Recalculate visit_count and last_visit_date",
+
+ async setup() {
+ async function setVisitCount(aURL, aValue) {
+ await PlacesUtils.withConnectionWrapper("setVisitCount", async db => {
+ await db.executeCached(
+ `UPDATE moz_places SET visit_count = :count
+ WHERE url_hash = hash(:url) AND url = :url`,
+ { count: aValue, url: aURL }
+ );
+ });
+ }
+ async function setLastVisitDate(aURL, aValue) {
+ await PlacesUtils.withConnectionWrapper("setVisitCount", async db => {
+ await db.executeCached(
+ `UPDATE moz_places SET last_visit_date = :date
+ WHERE url_hash = hash(:url) AND url = :url`,
+ { date: aValue, url: aURL }
+ );
+ });
+ }
+
+ let now = Date.now() * 1000;
+ // Add a page with 1 visit.
+ let url = "http://1.moz.org/";
+ await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ // Add a page with 1 visit and set wrong visit_count.
+ url = "http://2.moz.org/";
+ await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ await setVisitCount(url, 10);
+ // Add a page with 1 visit and set wrong last_visit_date.
+ url = "http://3.moz.org/";
+ await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ await setLastVisitDate(url, now++);
+ // Add a page with 1 visit and set wrong stats.
+ url = "http://4.moz.org/";
+ await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ await setVisitCount(url, 10);
+ await setLastVisitDate(url, now++);
+
+ // Add a page without visits.
+ url = "http://5.moz.org/";
+ await addPlace(url);
+ // Add a page without visits and set wrong visit_count.
+ url = "http://6.moz.org/";
+ await addPlace(url);
+ await setVisitCount(url, 10);
+ // Add a page without visits and set wrong last_visit_date.
+ url = "http://7.moz.org/";
+ await addPlace(url);
+ await setLastVisitDate(url, now++);
+ // Add a page without visits and set wrong stats.
+ url = "http://8.moz.org/";
+ await addPlace(url);
+ await setVisitCount(url, 10);
+ await setLastVisitDate(url, now++);
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ `SELECT h.id, h.last_visit_date as v_date
+ FROM moz_places h
+ LEFT JOIN moz_historyvisits v ON v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9)
+ GROUP BY h.id HAVING h.visit_count <> count(v.id)
+ UNION ALL
+ SELECT h.id, MAX(v.visit_date) as v_date
+ FROM moz_places h
+ LEFT JOIN moz_historyvisits v ON v.place_id = h.id
+ GROUP BY h.id HAVING h.last_visit_date IS NOT v_date`
+ );
+ Assert.equal(rows.length, 0);
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.3",
+ desc: "recalculate hidden for redirects.",
+
+ async setup() {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: NetUtil.newURI("http://l3.moz.org/"),
+ transition: TRANSITION_TYPED,
+ },
+ {
+ uri: NetUtil.newURI("http://l3.moz.org/redirecting/"),
+ transition: TRANSITION_TYPED,
+ },
+ {
+ uri: NetUtil.newURI("http://l3.moz.org/redirecting2/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ referrer: NetUtil.newURI("http://l3.moz.org/redirecting/"),
+ },
+ {
+ uri: NetUtil.newURI("http://l3.moz.org/target/"),
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ referrer: NetUtil.newURI("http://l3.moz.org/redirecting2/"),
+ },
+ ]);
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ "SELECT h.url FROM moz_places h WHERE h.hidden = 1"
+ );
+ Assert.equal(rows.length, 2);
+ for (let row of rows) {
+ let url = row.getResultByIndex(0);
+ Assert.ok(/redirecting/.test(url));
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.4",
+ desc: "recalculate foreign_count.",
+
+ async setup() {
+ this._pageGuid = (
+ await PlacesUtils.history.insert({
+ url: "http://l4.moz.org/",
+ visits: [{ date: new Date() }],
+ })
+ ).guid;
+ await PlacesUtils.bookmarks.insert({
+ url: "http://l4.moz.org/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.keywords.insert({
+ url: "http://l4.moz.org/",
+ keyword: "kw",
+ });
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "foreign_count", {
+ guid: this._pageGuid,
+ }),
+ 2
+ );
+ },
+
+ async check() {
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "foreign_count", {
+ guid: this._pageGuid,
+ }),
+ 2
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.5",
+ desc: "recalculate hashes when missing.",
+
+ async setup() {
+ this._pageGuid = (
+ await PlacesUtils.history.insert({
+ url: "http://l5.moz.org/",
+ visits: [{ date: new Date() }],
+ })
+ ).guid;
+ Assert.greater(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "url_hash", {
+ guid: this._pageGuid,
+ }),
+ 0
+ );
+ await PlacesUtils.withConnectionWrapper(
+ "change url hash",
+ async function (db) {
+ await db.execute(`UPDATE moz_places SET url_hash = 0`);
+ }
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "url_hash", {
+ guid: this._pageGuid,
+ }),
+ 0
+ );
+ },
+
+ async check() {
+ Assert.greater(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "url_hash", {
+ guid: this._pageGuid,
+ }),
+ 0
+ );
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.6",
+ desc: "fix invalid Place GUIDs",
+ _placeIds: [],
+
+ async setup() {
+ let placeWithValidGuid = await addPlace(
+ "http://example.com/a",
+ null,
+ "placeAAAAAAA"
+ );
+ this._placeIds.push(placeWithValidGuid);
+
+ let placeWithEmptyGuid = await addPlace("http://example.com/b", null, "");
+ this._placeIds.push(placeWithEmptyGuid);
+
+ let placeWithoutGuid = await addPlace("http://example.com/c", null, null);
+ this._placeIds.push(placeWithoutGuid);
+
+ let placeWithInvalidGuid = await addPlace(
+ "http://example.com/c",
+ null,
+ "{123456}"
+ );
+ this._placeIds.push(placeWithInvalidGuid);
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let updatedRows = await db.execute(
+ `
+ SELECT id, guid
+ FROM moz_places
+ WHERE id IN (?, ?, ?, ?)`,
+ this._placeIds
+ );
+
+ for (let row of updatedRows) {
+ let id = row.getResultByName("id");
+ let guid = row.getResultByName("guid");
+ if (id == this._placeIds[0]) {
+ Assert.equal(guid, "placeAAAAAAA");
+ } else {
+ Assert.ok(PlacesUtils.isValidGuid(guid));
+ }
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "S.1",
+ desc: "fix invalid GUIDs for synced bookmarks",
+ _bookmarkInfos: [],
+
+ async setup() {
+ let folderWithInvalidGuid = await addBookmark(
+ null,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ PlacesUtils.bookmarks.menuGuid,
+ /* aKeywordId */ null,
+ "NORMAL folder with invalid GUID",
+ "{123456}",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+
+ let placeIdForBookmarkWithoutGuid = await addPlace();
+ let bookmarkWithoutGuid = await addBookmark(
+ placeIdForBookmarkWithoutGuid,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "{123456}",
+ /* aKeywordId */ null,
+ "NEW bookmark without GUID",
+ /* aGuid */ null
+ );
+
+ let placeIdForBookmarkWithInvalidGuid = await addPlace();
+ let bookmarkWithInvalidGuid = await addBookmark(
+ placeIdForBookmarkWithInvalidGuid,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "{123456}",
+ /* aKeywordId */ null,
+ "NORMAL bookmark with invalid GUID",
+ "bookmarkAAAA\n",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+
+ let placeIdForBookmarkWithValidGuid = await addPlace();
+ let bookmarkWithValidGuid = await addBookmark(
+ placeIdForBookmarkWithValidGuid,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "{123456}",
+ /* aKeywordId */ null,
+ "NORMAL bookmark with valid GUID",
+ "bookmarkBBBB",
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ );
+
+ this._bookmarkInfos.push(
+ {
+ id: await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.menuGuid),
+ syncChangeCounter: 1,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ },
+ {
+ id: folderWithInvalidGuid,
+ syncChangeCounter: 3,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ },
+ {
+ id: bookmarkWithoutGuid,
+ syncChangeCounter: 1,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ },
+ {
+ id: bookmarkWithInvalidGuid,
+ syncChangeCounter: 1,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ },
+ {
+ id: bookmarkWithValidGuid,
+ syncChangeCounter: 0,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ }
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let updatedRows = await db.execute(
+ `SELECT id, guid, syncChangeCounter, syncStatus
+ FROM moz_bookmarks
+ WHERE id IN (?, ?, ?, ?, ?)`,
+ this._bookmarkInfos.map(info => info.id)
+ );
+
+ for (let row of updatedRows) {
+ let id = row.getResultByName("id");
+ let guid = row.getResultByName("guid");
+ Assert.ok(PlacesUtils.isValidGuid(guid));
+
+ let cachedGuid = await PlacesUtils.promiseItemGuid(id);
+ Assert.equal(cachedGuid, guid);
+
+ let expectedInfo = this._bookmarkInfos.find(info => info.id == id);
+
+ let syncChangeCounter = row.getResultByName("syncChangeCounter");
+ Assert.equal(syncChangeCounter, expectedInfo.syncChangeCounter);
+
+ let syncStatus = row.getResultByName("syncStatus");
+ Assert.equal(syncStatus, expectedInfo.syncStatus);
+ }
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.deepEqual(
+ tombstones.map(info => info.guid),
+ ["bookmarkAAAA\n", "{123456}"]
+ );
+ },
+});
+
+tests.push({
+ name: "S.2",
+ desc: "drop tombstones for bookmarks that aren't deleted",
+
+ async setup() {
+ let placeId = await addPlace();
+ await addBookmark(
+ placeId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ PlacesUtils.bookmarks.menuGuid,
+ null,
+ "",
+ "bookmarkAAAA"
+ );
+
+ await PlacesUtils.withConnectionWrapper("Insert tombstones", db =>
+ db.executeTransaction(async function () {
+ for (let guid of ["bookmarkAAAA", "bookmarkBBBB"]) {
+ await db.executeCached(
+ `INSERT INTO moz_bookmarks_deleted(guid)
+ VALUES(:guid)`,
+ { guid }
+ );
+ }
+ })
+ );
+ },
+
+ async check() {
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.deepEqual(
+ tombstones.map(info => info.guid),
+ ["bookmarkBBBB"]
+ );
+ },
+});
+
+tests.push({
+ name: "S.3",
+ desc: "set missing added and last modified dates",
+ _placeVisits: [],
+ _bookmarksWithDates: [],
+
+ async setup() {
+ let placeIdWithVisits = await addPlace();
+ let placeIdWithZeroVisit = await addPlace();
+ this._placeVisits.push(
+ {
+ placeId: placeIdWithVisits,
+ visitDate: PlacesUtils.toPRTime(new Date(2017, 9, 4)),
+ },
+ {
+ placeId: placeIdWithVisits,
+ visitDate: PlacesUtils.toPRTime(new Date(2017, 9, 8)),
+ },
+ {
+ placeId: placeIdWithZeroVisit,
+ visitDate: 0,
+ }
+ );
+
+ this._bookmarksWithDates.push(
+ {
+ guid: "bookmarkAAAA",
+ placeId: null,
+ parent: PlacesUtils.bookmarks.menuGuid,
+ dateAdded: null,
+ lastModified: PlacesUtils.toPRTime(new Date(2017, 9, 1)),
+ },
+ {
+ guid: "bookmarkBBBB",
+ placeId: null,
+ parent: PlacesUtils.bookmarks.menuGuid,
+ dateAdded: PlacesUtils.toPRTime(new Date(2017, 9, 2)),
+ lastModified: null,
+ },
+ {
+ guid: "bookmarkCCCC",
+ placeId: null,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ dateAdded: null,
+ lastModified: null,
+ },
+ {
+ guid: "bookmarkDDDD",
+ placeId: placeIdWithVisits,
+ parent: PlacesUtils.bookmarks.mobileGuid,
+ dateAdded: null,
+ lastModified: null,
+ },
+ {
+ guid: "bookmarkEEEE",
+ placeId: placeIdWithVisits,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ dateAdded: PlacesUtils.toPRTime(new Date(2017, 9, 3)),
+ lastModified: PlacesUtils.toPRTime(new Date(2017, 9, 6)),
+ },
+ {
+ guid: "bookmarkFFFF",
+ placeId: placeIdWithZeroVisit,
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ dateAdded: 0,
+ lastModified: 0,
+ }
+ );
+
+ await PlacesUtils.withConnectionWrapper(
+ "S.3: Insert bookmarks and visits",
+ db =>
+ db.executeTransaction(async () => {
+ await db.execute(
+ `INSERT INTO moz_historyvisits(place_id, visit_date)
+ VALUES(:placeId, :visitDate)`,
+ this._placeVisits
+ );
+
+ await db.execute(
+ `INSERT INTO moz_bookmarks(fk, type, parent, guid, dateAdded,
+ lastModified)
+ VALUES(:placeId, 1, (SELECT id FROM moz_bookmarks WHERE guid = :parent),
+ :guid, :dateAdded, :lastModified)`,
+ this._bookmarksWithDates
+ );
+
+ await db.execute(
+ `UPDATE moz_bookmarks SET dateAdded = 0, lastModified = NULL
+ WHERE guid = :toolbarFolder`,
+ { toolbarFolder: PlacesUtils.bookmarks.toolbarGuid }
+ );
+ })
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let updatedRows = await db.execute(
+ `SELECT guid, dateAdded, lastModified
+ FROM moz_bookmarks
+ WHERE guid = :guid`,
+ [
+ { guid: PlacesUtils.bookmarks.toolbarGuid },
+ ...this._bookmarksWithDates.map(({ guid }) => ({ guid })),
+ ]
+ );
+
+ for (let row of updatedRows) {
+ let guid = row.getResultByName("guid");
+
+ let dateAdded = row.getResultByName("dateAdded");
+ Assert.ok(Number.isInteger(dateAdded));
+
+ let lastModified = row.getResultByName("lastModified");
+ Assert.ok(Number.isInteger(lastModified));
+
+ switch (guid) {
+ // Last modified date exists, so we should use it for date added.
+ case "bookmarkAAAA": {
+ let expectedInfo = this._bookmarksWithDates[0];
+ Assert.equal(dateAdded, expectedInfo.lastModified);
+ Assert.equal(lastModified, expectedInfo.lastModified);
+ break;
+ }
+
+ // Date added exists, so we should use it for last modified date.
+ case "bookmarkBBBB": {
+ let expectedInfo = this._bookmarksWithDates[1];
+ Assert.equal(dateAdded, expectedInfo.dateAdded);
+ Assert.equal(lastModified, expectedInfo.dateAdded);
+ break;
+ }
+
+ // C has no visits, date added, or last modified time, F has zeros for
+ // all, and the toolbar has a zero date added and no last modified time.
+ // In all cases, we should fall back to the current time.
+ case "bookmarkCCCC":
+ case "bookmarkFFFF":
+ case PlacesUtils.bookmarks.toolbarGuid: {
+ let nowAsPRTime = PlacesUtils.toPRTime(new Date());
+ Assert.greater(dateAdded, 0);
+ Assert.equal(dateAdded, lastModified);
+ Assert.ok(dateAdded <= nowAsPRTime);
+ break;
+ }
+
+ // Neither date added nor last modified exists, but we have two
+ // visits, so we should fall back to the earliest and latest visit
+ // dates.
+ case "bookmarkDDDD": {
+ let oldestVisit = this._placeVisits[0];
+ Assert.equal(dateAdded, oldestVisit.visitDate);
+ let newestVisit = this._placeVisits[1];
+ Assert.equal(lastModified, newestVisit.visitDate);
+ break;
+ }
+
+ // We have two visits, but both date added and last modified exist,
+ // so we shouldn't update them.
+ case "bookmarkEEEE": {
+ let expectedInfo = this._bookmarksWithDates[4];
+ Assert.equal(dateAdded, expectedInfo.dateAdded);
+ Assert.equal(lastModified, expectedInfo.lastModified);
+ break;
+ }
+
+ default:
+ throw new Error(`Unexpected row for bookmark ${guid}`);
+ }
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "S.4",
+ desc: "reset added dates that are ahead of last modified dates",
+ _bookmarksWithDates: [],
+
+ async setup() {
+ this._bookmarksWithDates.push({
+ guid: "bookmarkGGGG",
+ parent: PlacesUtils.bookmarks.unfiledGuid,
+ dateAdded: PlacesUtils.toPRTime(new Date(2017, 9, 6)),
+ lastModified: PlacesUtils.toPRTime(new Date(2017, 9, 3)),
+ });
+
+ await PlacesUtils.withConnectionWrapper(
+ "S.4: Insert bookmarks and visits",
+ db =>
+ db.executeTransaction(async () => {
+ await db.execute(
+ `INSERT INTO moz_bookmarks(type, parent, guid, dateAdded,
+ lastModified)
+ VALUES(1, (SELECT id FROM moz_bookmarks WHERE guid = :parent),
+ :guid, :dateAdded, :lastModified)`,
+ this._bookmarksWithDates
+ );
+ })
+ );
+ },
+
+ async check() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let updatedRows = await db.execute(
+ `SELECT guid, dateAdded, lastModified
+ FROM moz_bookmarks
+ WHERE guid = :guid`,
+ this._bookmarksWithDates.map(({ guid }) => ({ guid }))
+ );
+
+ for (let row of updatedRows) {
+ let guid = row.getResultByName("guid");
+ let dateAdded = row.getResultByName("dateAdded");
+ let lastModified = row.getResultByName("lastModified");
+ switch (guid) {
+ case "bookmarkGGGG": {
+ let expectedInfo = this._bookmarksWithDates[0];
+ Assert.equal(dateAdded, expectedInfo.lastModified);
+ Assert.equal(lastModified, expectedInfo.lastModified);
+ break;
+ }
+
+ default:
+ throw new Error(`Unexpected row for bookmark ${guid}`);
+ }
+ }
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "T.1",
+ desc: "history.recalculateOriginFrecencyStats() is called",
+
+ async setup() {
+ let urls = [
+ "http://example1.com/",
+ "http://example2.com/",
+ "http://example3.com/",
+ ];
+ await PlacesTestUtils.addVisits(urls.map(u => ({ uri: u })));
+
+ this._frecencies = [];
+ for (let url of urls) {
+ this._frecencies.push(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url,
+ })
+ );
+ }
+
+ let stats = await this._promiseStats();
+ Assert.equal(stats.count, this._frecencies.length, "Sanity check");
+ Assert.equal(stats.sum, this._sum(this._frecencies), "Sanity check");
+ Assert.equal(
+ stats.squares,
+ this._squares(this._frecencies),
+ "Sanity check"
+ );
+
+ await PlacesUtils.withConnectionWrapper("T.1", db =>
+ db.execute(`
+ INSERT OR REPLACE INTO moz_meta VALUES
+ ('origin_frecency_count', 99),
+ ('origin_frecency_sum', 99999),
+ ('origin_frecency_sum_of_squares', 99999 * 99999);
+ `)
+ );
+
+ stats = await this._promiseStats();
+ Assert.equal(stats.count, 99);
+ Assert.equal(stats.sum, 99999);
+ Assert.equal(stats.squares, 99999 * 99999);
+ },
+
+ async check() {
+ let stats = await this._promiseStats();
+ Assert.equal(stats.count, this._frecencies.length);
+ Assert.equal(stats.sum, this._sum(this._frecencies));
+ Assert.equal(stats.squares, this._squares(this._frecencies));
+ },
+
+ _sum(frecs) {
+ return frecs.reduce((memo, f) => memo + f, 0);
+ },
+
+ _squares(frecs) {
+ return frecs.reduce((memo, f) => memo + f * f, 0);
+ },
+
+ async _promiseStats() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0)
+ `);
+ return {
+ count: rows[0].getResultByIndex(0),
+ sum: rows[0].getResultByIndex(1),
+ squares: rows[0].getResultByIndex(2),
+ };
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "Z",
+ desc: "Sanity: Preventive maintenance does not touch valid items",
+
+ _uri1: uri("http://www1.mozilla.org"),
+ _uri2: uri("http://www2.mozilla.org"),
+ _folder: null,
+ _bookmark: null,
+ _bookmarkId: null,
+ _separator: null,
+
+ async setup() {
+ // use valid api calls to create a bunch of items
+ await PlacesTestUtils.addVisits([{ uri: this._uri1 }, { uri: this._uri2 }]);
+
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ title: "testfolder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "testbookmark",
+ url: this._uri1,
+ },
+ ],
+ },
+ ],
+ });
+
+ this._folder = bookmarks[0];
+ this._bookmark = bookmarks[1];
+ this._bookmarkId = await PlacesUtils.promiseItemId(bookmarks[1].guid);
+
+ this._separator = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ });
+
+ PlacesUtils.tagging.tagURI(this._uri1, ["testtag"]);
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ this._uri2,
+ SMALLPNG_DATA_URI,
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ await PlacesUtils.keywords.insert({
+ url: this._uri1.spec,
+ keyword: "testkeyword",
+ });
+ await PlacesUtils.history.update({
+ url: this._uri2,
+ annotations: new Map([["anno", "anno"]]),
+ });
+ },
+
+ async check() {
+ // Check that all items are correct
+ let isVisited = await PlacesUtils.history.hasVisits(this._uri1);
+ Assert.ok(isVisited);
+ isVisited = await PlacesUtils.history.hasVisits(this._uri2);
+ Assert.ok(isVisited);
+
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(this._bookmark.guid)).url,
+ this._uri1.spec
+ );
+ let folder = await PlacesUtils.bookmarks.fetch(this._folder.guid);
+ Assert.equal(folder.index, 0);
+ Assert.equal(folder.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(this._separator.guid)).type,
+ PlacesUtils.bookmarks.TYPE_SEPARATOR
+ );
+
+ Assert.equal(PlacesUtils.tagging.getTagsForURI(this._uri1).length, 1);
+ Assert.equal(
+ (await PlacesUtils.keywords.fetch({ url: this._uri1.spec })).keyword,
+ "testkeyword"
+ );
+ let pageInfo = await PlacesUtils.history.fetch(this._uri2, {
+ includeAnnotations: true,
+ });
+ Assert.equal(pageInfo.annotations.get("anno"), "anno");
+
+ await new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconURLForPage(this._uri2, aFaviconURI => {
+ Assert.ok(aFaviconURI.equals(SMALLPNG_DATA_URI));
+ resolve();
+ });
+ });
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+add_task(async function test_preventive_maintenance() {
+ let db = await PlacesUtils.promiseDBConnection();
+ // Get current bookmarks max ID for cleanup
+ defaultBookmarksMaxId = (
+ await db.executeCached("SELECT MAX(id) FROM moz_bookmarks")
+ )[0].getResultByIndex(0);
+ Assert.ok(defaultBookmarksMaxId > 0);
+
+ for (let test of tests) {
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("\nExecuting test: " + test.name + "\n*** " + test.desc + "\n");
+ await test.setup();
+
+ Services.prefs.clearUserPref("places.database.lastMaintenance");
+ await PlacesDBUtils.maintenanceOnIdle();
+
+ // Check the lastMaintenance time has been saved.
+ Assert.notEqual(
+ Services.prefs.getIntPref("places.database.lastMaintenance"),
+ null
+ );
+
+ await test.check();
+
+ await cleanDatabase();
+ }
+
+ // Sanity check: all roots should be intact
+ Assert.strictEqual(
+ (await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.rootGuid))
+ .parentGuid,
+ undefined
+ );
+ Assert.deepEqual(
+ (await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.menuGuid))
+ .parentGuid,
+ PlacesUtils.bookmarks.rootGuid
+ );
+ Assert.deepEqual(
+ (await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.tagsGuid))
+ .parentGuid,
+ PlacesUtils.bookmarks.rootGuid
+ );
+ Assert.deepEqual(
+ (await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid))
+ .parentGuid,
+ PlacesUtils.bookmarks.rootGuid
+ );
+ Assert.deepEqual(
+ (await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid))
+ .parentGuid,
+ PlacesUtils.bookmarks.rootGuid
+ );
+});
+
+// ------------------------------------------------------------------------------
+
+add_task(async function test_idle_daily() {
+ const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(PlacesDBUtils, "maintenanceOnIdle");
+ Services.prefs.clearUserPref("places.database.lastMaintenance");
+ Cc["@mozilla.org/places/databaseUtilsIdleMaintenance;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "idle-daily", "");
+ Assert.ok(
+ PlacesDBUtils.maintenanceOnIdle.calledOnce,
+ "maintenanceOnIdle was invoked"
+ );
+ sandbox.restore();
+});