Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
/* -*- 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
/**
* 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");
});
});
}
async function addPlace(
aUrl,
aFavicon,
aGuid = PlacesUtils.history.makeGuid(),
aHash = null
) {
let href = new URL(
).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);
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 PlacesTestUtils.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 PlacesTestUtils.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 PlacesTestUtils.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 PlacesTestUtils.promiseItemGuid(this._orphanFolderId),
syncChangeCounter: 0,
},
{
id: await PlacesTestUtils.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 PlacesTestUtils.promiseItemGuid(
this._separatorId
);
// Add a folder with a fk
this._folderId = await addBookmark(
this._placeId,
PlacesUtils.bookmarks.TYPE_FOLDER
);
this._folderGuid = await PlacesTestUtils.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 PlacesTestUtils.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);
}
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 PlacesTestUtils.promiseItemGuid(
this._validBookmarkId
);
// Add a bookmark with a null place id
this._invalidBookmarkId = await addBookmark(
null,
PlacesUtils.bookmarks.TYPE_BOOKMARK
);
this._invalidBookmarkGuid = await PlacesTestUtils.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 PlacesTestUtils.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 PlacesTestUtils.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);
}
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 PlacesTestUtils.promiseItemGuid(
this._bookmarkId
);
this._syncedBookmarkId = await addBookmark(
this._placeId,
null,
PlacesUtils.bookmarks.toolbarGuid,
null,
null,
"bookmarkAAAA",
PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
);
this._syncedBookmarkGuid = await PlacesTestUtils.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 PlacesTestUtils.promiseItemGuid(this._folderId);
this._syncedFolderId = await addBookmark(
null,
null,
PlacesUtils.bookmarks.toolbarGuid,
null,
null,
"folderBBBBBB",
PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
);
this._syncedFolderGuid = await PlacesTestUtils.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);
}
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 PlacesTestUtils.promiseItemGuid(this._bookmarkId);
this._bookmarkId1 = await addBookmark(
this._placeId,
PlacesUtils.bookmarks.TYPE_BOOKMARK,
bookmarkGuid
);
let separatorGuid = await PlacesTestUtils.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 PlacesTestUtils.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",
});
}
// 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 PlacesTestUtils.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 PlacesTestUtils.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,
root: 0,
},
{
favicon_id: 2,
root: 0,
},
{
favicon_id: 3,
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))`,
);
});
});
// Insert a place using the existing favicon entry
},
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({
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.
// Duplicate Place with different visits and a keyword.
// Another duplicate with conflicting autocomplete history entry and
// two more bookmarks.
// Unrelated, unique Place.
);
// Another unrelated Place, with the same hash as D, but different URL.
);
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.
await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
// Add a page with 1 visit and set wrong visit_count.
await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
await setVisitCount(url, 10);
// Add a page with 1 visit and set wrong last_visit_date.
await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
await setLastVisitDate(url, now++);
// Add a page with 1 visit and set wrong stats.
await PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
await setVisitCount(url, 10);
await setLastVisitDate(url, now++);
// Add a page without visits.
await addPlace(url);
// Add a page without visits and set wrong visit_count.
await addPlace(url);
await setVisitCount(url, 10);
// Add a page without visits and set wrong last_visit_date.
await addPlace(url);
await setLastVisitDate(url, now++);
// Add a page without visits and set wrong stats.
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([
{
transition: TRANSITION_TYPED,
},
{
transition: TRANSITION_TYPED,
},
{
transition: TRANSITION_REDIRECT_TEMPORARY,
},
{
transition: TRANSITION_REDIRECT_PERMANENT,
},
]);
},
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({
visits: [{ date: new Date() }],
})
).guid;
await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
await PlacesUtils.keywords.insert({
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({
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(
null,
"placeAAAAAAA"
);
this._placeIds.push(placeWithValidGuid);
this._placeIds.push(placeWithEmptyGuid);
this._placeIds.push(placeWithoutGuid);
let placeWithInvalidGuid = await addPlace(
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 PlacesTestUtils.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 PlacesTestUtils.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: "Z",
desc: "Sanity: Preventive maintenance does not touch valid items",
_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 PlacesTestUtils.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"]);
await PlacesUtils.favicons.setFaviconForPage(
this._uri2,
SMALLPNG_DATA_URI,
SMALLPNG_DATA_URI
);
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(
);
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();
});