tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_Edge_db_migration.js (25455B)


      1 "use strict";
      2 
      3 const { ctypes } = ChromeUtils.importESModule(
      4  "resource://gre/modules/ctypes.sys.mjs"
      5 );
      6 const { ESE, KERNEL, gLibs, COLUMN_TYPES, declareESEFunction, loadLibraries } =
      7  ChromeUtils.importESModule("resource:///modules/ESEDBReader.sys.mjs");
      8 const { EdgeProfileMigrator } = ChromeUtils.importESModule(
      9  "resource:///modules/EdgeProfileMigrator.sys.mjs"
     10 );
     11 
     12 let gESEInstanceCounter = 1;
     13 
     14 ESE.JET_COLUMNCREATE_W = new ctypes.StructType("JET_COLUMNCREATE_W", [
     15  { cbStruct: ctypes.unsigned_long },
     16  { szColumnName: ESE.JET_PCWSTR },
     17  { coltyp: ESE.JET_COLTYP },
     18  { cbMax: ctypes.unsigned_long },
     19  { grbit: ESE.JET_GRBIT },
     20  { pvDefault: ctypes.voidptr_t },
     21  { cbDefault: ctypes.unsigned_long },
     22  { cp: ctypes.unsigned_long },
     23  { columnid: ESE.JET_COLUMNID },
     24  { err: ESE.JET_ERR },
     25 ]);
     26 
     27 function createColumnCreationWrapper({ name, type, cbMax }) {
     28  // We use a wrapper object because we need to be sure the JS engine won't GC
     29  // data that we're "only" pointing to.
     30  let wrapper = {};
     31  wrapper.column = new ESE.JET_COLUMNCREATE_W();
     32  wrapper.column.cbStruct = ESE.JET_COLUMNCREATE_W.size;
     33  let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
     34  wrapper.name = new wchar_tArray(name.length + 1);
     35  wrapper.name.value = String(name);
     36  wrapper.column.szColumnName = wrapper.name;
     37  wrapper.column.coltyp = type;
     38  let fallback = 0;
     39  switch (type) {
     40    case COLUMN_TYPES.JET_coltypText:
     41      fallback = 255;
     42    // Intentional fall-through
     43    case COLUMN_TYPES.JET_coltypLongText:
     44      wrapper.column.cbMax = cbMax || fallback || 64 * 1024;
     45      break;
     46    case COLUMN_TYPES.JET_coltypGUID:
     47      wrapper.column.cbMax = 16;
     48      break;
     49    case COLUMN_TYPES.JET_coltypBit:
     50      wrapper.column.cbMax = 1;
     51      break;
     52    case COLUMN_TYPES.JET_coltypLongLong:
     53      wrapper.column.cbMax = 8;
     54      break;
     55    default:
     56      throw new Error("Unknown column type!");
     57  }
     58 
     59  wrapper.column.columnid = new ESE.JET_COLUMNID();
     60  wrapper.column.grbit = 0;
     61  wrapper.column.pvDefault = null;
     62  wrapper.column.cbDefault = 0;
     63  wrapper.column.cp = 0;
     64 
     65  return wrapper;
     66 }
     67 
     68 // "forward declarations" of indexcreate and setinfo structs, which we don't use.
     69 ESE.JET_INDEXCREATE = new ctypes.StructType("JET_INDEXCREATE");
     70 ESE.JET_SETINFO = new ctypes.StructType("JET_SETINFO");
     71 
     72 ESE.JET_TABLECREATE_W = new ctypes.StructType("JET_TABLECREATE_W", [
     73  { cbStruct: ctypes.unsigned_long },
     74  { szTableName: ESE.JET_PCWSTR },
     75  { szTemplateTableName: ESE.JET_PCWSTR },
     76  { ulPages: ctypes.unsigned_long },
     77  { ulDensity: ctypes.unsigned_long },
     78  { rgcolumncreate: ESE.JET_COLUMNCREATE_W.ptr },
     79  { cColumns: ctypes.unsigned_long },
     80  { rgindexcreate: ESE.JET_INDEXCREATE.ptr },
     81  { cIndexes: ctypes.unsigned_long },
     82  { grbit: ESE.JET_GRBIT },
     83  { tableid: ESE.JET_TABLEID },
     84  { cCreated: ctypes.unsigned_long },
     85 ]);
     86 
     87 function createTableCreationWrapper(tableName, columns) {
     88  let wrapper = {};
     89  let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
     90  wrapper.name = new wchar_tArray(tableName.length + 1);
     91  wrapper.name.value = String(tableName);
     92  wrapper.table = new ESE.JET_TABLECREATE_W();
     93  wrapper.table.cbStruct = ESE.JET_TABLECREATE_W.size;
     94  wrapper.table.szTableName = wrapper.name;
     95  wrapper.table.szTemplateTableName = null;
     96  wrapper.table.ulPages = 1;
     97  wrapper.table.ulDensity = 0;
     98  let columnArrayType = ESE.JET_COLUMNCREATE_W.array(columns.length);
     99  wrapper.columnAry = new columnArrayType();
    100  wrapper.table.rgcolumncreate = wrapper.columnAry.addressOfElement(0);
    101  wrapper.table.cColumns = columns.length;
    102  wrapper.columns = [];
    103  for (let i = 0; i < columns.length; i++) {
    104    let column = columns[i];
    105    let columnWrapper = createColumnCreationWrapper(column);
    106    wrapper.columnAry.addressOfElement(i).contents = columnWrapper.column;
    107    wrapper.columns.push(columnWrapper);
    108  }
    109  wrapper.table.rgindexcreate = null;
    110  wrapper.table.cIndexes = 0;
    111  return wrapper;
    112 }
    113 
    114 function convertValueForWriting(value, valueType) {
    115  let buffer;
    116  let valueOfValueType = ctypes.UInt64.lo(valueType);
    117  switch (valueOfValueType) {
    118    case COLUMN_TYPES.JET_coltypLongLong:
    119      if (value instanceof Date) {
    120        buffer = new KERNEL.FILETIME();
    121        let sysTime = new KERNEL.SYSTEMTIME();
    122        sysTime.wYear = value.getUTCFullYear();
    123        sysTime.wMonth = value.getUTCMonth() + 1;
    124        sysTime.wDay = value.getUTCDate();
    125        sysTime.wHour = value.getUTCHours();
    126        sysTime.wMinute = value.getUTCMinutes();
    127        sysTime.wSecond = value.getUTCSeconds();
    128        sysTime.wMilliseconds = value.getUTCMilliseconds();
    129        let rv = KERNEL.SystemTimeToFileTime(
    130          sysTime.address(),
    131          buffer.address()
    132        );
    133        if (!rv) {
    134          throw new Error("Failed to get FileTime.");
    135        }
    136        return [buffer, KERNEL.FILETIME.size];
    137      }
    138      throw new Error("Unrecognized value for longlong column");
    139    case COLUMN_TYPES.JET_coltypLongText: {
    140      let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
    141      buffer = new wchar_tArray(value.length + 1);
    142      buffer.value = String(value);
    143      return [buffer, buffer.length * 2];
    144    }
    145    case COLUMN_TYPES.JET_coltypBit:
    146      buffer = new ctypes.uint8_t();
    147      // Bizarre boolean values, but whatever:
    148      buffer.value = value ? 255 : 0;
    149      return [buffer, 1];
    150    case COLUMN_TYPES.JET_coltypGUID: {
    151      let byteArray = ctypes.ArrayType(ctypes.uint8_t);
    152      buffer = new byteArray(16);
    153      let j = 0;
    154      for (let i = 0; i < value.length; i++) {
    155        if (!/[0-9a-f]/i.test(value[i])) {
    156          continue;
    157        }
    158        let byteAsHex = value.substr(i, 2);
    159        buffer[j++] = parseInt(byteAsHex, 16);
    160        i++;
    161      }
    162      return [buffer, 16];
    163    }
    164  }
    165 
    166  throw new Error("Unknown type " + valueType);
    167 }
    168 
    169 let initializedESE = false;
    170 
    171 let eseDBWritingHelpers = {
    172  setupDB(dbFile, tables) {
    173    if (!initializedESE) {
    174      initializedESE = true;
    175      loadLibraries();
    176 
    177      KERNEL.SystemTimeToFileTime = gLibs.kernel.declare(
    178        "SystemTimeToFileTime",
    179        ctypes.winapi_abi,
    180        ctypes.bool,
    181        KERNEL.SYSTEMTIME.ptr,
    182        KERNEL.FILETIME.ptr
    183      );
    184 
    185      declareESEFunction(
    186        "CreateDatabaseW",
    187        ESE.JET_SESID,
    188        ESE.JET_PCWSTR,
    189        ESE.JET_PCWSTR,
    190        ESE.JET_DBID.ptr,
    191        ESE.JET_GRBIT
    192      );
    193      declareESEFunction(
    194        "CreateTableColumnIndexW",
    195        ESE.JET_SESID,
    196        ESE.JET_DBID,
    197        ESE.JET_TABLECREATE_W.ptr
    198      );
    199      declareESEFunction("BeginTransaction", ESE.JET_SESID);
    200      declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT);
    201      declareESEFunction(
    202        "PrepareUpdate",
    203        ESE.JET_SESID,
    204        ESE.JET_TABLEID,
    205        ctypes.unsigned_long
    206      );
    207      declareESEFunction(
    208        "Update",
    209        ESE.JET_SESID,
    210        ESE.JET_TABLEID,
    211        ctypes.voidptr_t,
    212        ctypes.unsigned_long,
    213        ctypes.unsigned_long.ptr
    214      );
    215      declareESEFunction(
    216        "SetColumn",
    217        ESE.JET_SESID,
    218        ESE.JET_TABLEID,
    219        ESE.JET_COLUMNID,
    220        ctypes.voidptr_t,
    221        ctypes.unsigned_long,
    222        ESE.JET_GRBIT,
    223        ESE.JET_SETINFO.ptr
    224      );
    225      ESE.SetSystemParameterW(
    226        null,
    227        0,
    228        64 /* JET_paramDatabasePageSize*/,
    229        8192,
    230        null
    231      );
    232    }
    233 
    234    let rootPath = dbFile.parent.path + "\\";
    235    let logPath = rootPath + "LogFiles\\";
    236 
    237    try {
    238      this._instanceId = new ESE.JET_INSTANCE();
    239      ESE.CreateInstanceW(
    240        this._instanceId.address(),
    241        "firefox-dbwriter-" + gESEInstanceCounter++
    242      );
    243      this._instanceCreated = true;
    244 
    245      ESE.SetSystemParameterW(
    246        this._instanceId.address(),
    247        0,
    248        0 /* JET_paramSystemPath*/,
    249        0,
    250        rootPath
    251      );
    252      ESE.SetSystemParameterW(
    253        this._instanceId.address(),
    254        0,
    255        1 /* JET_paramTempPath */,
    256        0,
    257        rootPath
    258      );
    259      ESE.SetSystemParameterW(
    260        this._instanceId.address(),
    261        0,
    262        2 /* JET_paramLogFilePath*/,
    263        0,
    264        logPath
    265      );
    266      // Shouldn't try to call JetTerm if the following call fails.
    267      this._instanceCreated = false;
    268      ESE.Init(this._instanceId.address());
    269      this._instanceCreated = true;
    270      this._sessionId = new ESE.JET_SESID();
    271      ESE.BeginSessionW(
    272        this._instanceId,
    273        this._sessionId.address(),
    274        null,
    275        null
    276      );
    277      this._sessionCreated = true;
    278 
    279      this._dbId = new ESE.JET_DBID();
    280      this._dbPath = rootPath + "spartan.edb";
    281      ESE.CreateDatabaseW(
    282        this._sessionId,
    283        this._dbPath,
    284        null,
    285        this._dbId.address(),
    286        0
    287      );
    288      this._opened = this._attached = true;
    289 
    290      for (let [tableName, data] of tables) {
    291        let { rows, columns } = data;
    292        let tableCreationWrapper = createTableCreationWrapper(
    293          tableName,
    294          columns
    295        );
    296        ESE.CreateTableColumnIndexW(
    297          this._sessionId,
    298          this._dbId,
    299          tableCreationWrapper.table.address()
    300        );
    301        this._tableId = tableCreationWrapper.table.tableid;
    302 
    303        let columnIdMap = new Map();
    304        if (rows.length) {
    305          // Iterate over the struct we passed into ESENT because they have the
    306          // created column ids.
    307          let columnCount = ctypes.UInt64.lo(
    308            tableCreationWrapper.table.cColumns
    309          );
    310          let columnsPassed = tableCreationWrapper.table.rgcolumncreate;
    311          for (let i = 0; i < columnCount; i++) {
    312            let column = columnsPassed.contents;
    313            columnIdMap.set(column.szColumnName.readString(), column);
    314            columnsPassed = columnsPassed.increment();
    315          }
    316          ESE.ManualMove(
    317            this._sessionId,
    318            this._tableId,
    319            -2147483648 /* JET_MoveFirst */,
    320            0
    321          );
    322          ESE.BeginTransaction(this._sessionId);
    323          for (let row of rows) {
    324            ESE.PrepareUpdate(
    325              this._sessionId,
    326              this._tableId,
    327              0 /* JET_prepInsert */
    328            );
    329            for (let columnName in row) {
    330              let col = columnIdMap.get(columnName);
    331              let colId = col.columnid;
    332              let [val, valSize] = convertValueForWriting(
    333                row[columnName],
    334                col.coltyp
    335              );
    336              /* JET_bitSetOverwriteLV */
    337              ESE.SetColumn(
    338                this._sessionId,
    339                this._tableId,
    340                colId,
    341                val.address(),
    342                valSize,
    343                4,
    344                null
    345              );
    346            }
    347            let actualBookmarkSize = new ctypes.unsigned_long();
    348            ESE.Update(
    349              this._sessionId,
    350              this._tableId,
    351              null,
    352              0,
    353              actualBookmarkSize.address()
    354            );
    355          }
    356          ESE.CommitTransaction(
    357            this._sessionId,
    358            0 /* JET_bitWaitLastLevel0Commit */
    359          );
    360        }
    361      }
    362    } finally {
    363      try {
    364        this._close();
    365      } catch (ex) {
    366        console.error(ex);
    367      }
    368    }
    369  },
    370 
    371  _close() {
    372    if (this._tableId) {
    373      ESE.FailSafeCloseTable(this._sessionId, this._tableId);
    374      delete this._tableId;
    375    }
    376    if (this._opened) {
    377      ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
    378      this._opened = false;
    379    }
    380    if (this._attached) {
    381      ESE.FailSafeDetachDatabaseW(this._sessionId, this._dbPath);
    382      this._attached = false;
    383    }
    384    if (this._sessionCreated) {
    385      ESE.FailSafeEndSession(this._sessionId, 0);
    386      this._sessionCreated = false;
    387    }
    388    if (this._instanceCreated) {
    389      ESE.FailSafeTerm(this._instanceId);
    390      this._instanceCreated = false;
    391    }
    392  },
    393 };
    394 
    395 add_task(async function () {
    396  let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
    397  tempFile.append("fx-xpcshell-edge-db");
    398  tempFile.createUnique(tempFile.DIRECTORY_TYPE, 0o600);
    399 
    400  let db = tempFile.clone();
    401  db.append("spartan.edb");
    402 
    403  let logs = tempFile.clone();
    404  logs.append("LogFiles");
    405  logs.create(tempFile.DIRECTORY_TYPE, 0o600);
    406 
    407  let creationDate = new Date(Date.now() - 5000);
    408  const kEdgeMenuParent = "62d07e2b-5f0d-4e41-8426-5f5ec9717beb";
    409  let bookmarkReferenceItems = [
    410    {
    411      URL: "http://www.mozilla.org/",
    412      Title: "Mozilla",
    413      DateUpdated: new Date(creationDate.valueOf() + 100),
    414      ItemId: "1c00c10a-15f6-4618-92dd-22575102a4da",
    415      ParentId: kEdgeMenuParent,
    416      IsFolder: false,
    417      IsDeleted: false,
    418    },
    419    {
    420      Title: "Folder",
    421      DateUpdated: new Date(creationDate.valueOf() + 200),
    422      ItemId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
    423      ParentId: kEdgeMenuParent,
    424      IsFolder: true,
    425      IsDeleted: false,
    426    },
    427    {
    428      Title: "Item in folder",
    429      URL: "http://www.iteminfolder.org/",
    430      DateUpdated: new Date(creationDate.valueOf() + 300),
    431      ItemId: "c295ddaf-04a1-424a-866c-0ebde011e7c8",
    432      ParentId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
    433      IsFolder: false,
    434      IsDeleted: false,
    435    },
    436    {
    437      Title: "Deleted folder",
    438      DateUpdated: new Date(creationDate.valueOf() + 400),
    439      ItemId: "a547573c-4d4d-4406-a736-5b5462d93bca",
    440      ParentId: kEdgeMenuParent,
    441      IsFolder: true,
    442      IsDeleted: true,
    443    },
    444    {
    445      Title: "Deleted item",
    446      URL: "http://www.deleteditem.org/",
    447      DateUpdated: new Date(creationDate.valueOf() + 500),
    448      ItemId: "37a574bb-b44b-4bbc-a414-908615536435",
    449      ParentId: kEdgeMenuParent,
    450      IsFolder: false,
    451      IsDeleted: true,
    452    },
    453    {
    454      Title: "Item in deleted folder (should be in root)",
    455      URL: "http://www.itemindeletedfolder.org/",
    456      DateUpdated: new Date(creationDate.valueOf() + 600),
    457      ItemId: "74dd1cc3-4c5d-471f-bccc-7bc7c72fa621",
    458      ParentId: "a547573c-4d4d-4406-a736-5b5462d93bca",
    459      IsFolder: false,
    460      IsDeleted: false,
    461    },
    462    {
    463      Title: "_Favorites_Bar_",
    464      DateUpdated: new Date(creationDate.valueOf() + 700),
    465      ItemId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
    466      ParentId: kEdgeMenuParent,
    467      IsFolder: true,
    468      IsDeleted: false,
    469    },
    470    {
    471      Title: "Item in favorites bar",
    472      URL: "http://www.iteminfavoritesbar.org/",
    473      DateUpdated: new Date(creationDate.valueOf() + 800),
    474      ItemId: "9f2b1ff8-b651-46cf-8f41-16da8bcb6791",
    475      ParentId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
    476      IsFolder: false,
    477      IsDeleted: false,
    478    },
    479  ];
    480 
    481  let readingListReferenceItems = [
    482    {
    483      Title: "Some mozilla page",
    484      URL: "http://www.mozilla.org/somepage/",
    485      AddedDate: new Date(creationDate.valueOf() + 900),
    486      ItemId: "c88426fd-52a7-419d-acbc-d2310e8afebe",
    487      IsDeleted: false,
    488    },
    489    {
    490      Title: "Some other page",
    491      URL: "https://www.example.org/somepage/",
    492      AddedDate: new Date(creationDate.valueOf() + 1000),
    493      ItemId: "a35fc843-5d5a-4d1e-9be8-45214be24b5c",
    494      IsDeleted: false,
    495    },
    496  ];
    497 
    498  // The following entries are expected to be skipped as being too old to
    499  // migrate.
    500  let expiredTypedURLsReferenceItems = [
    501    {
    502      URL: "https://expired1.invalid/",
    503      AccessDateTimeUTC: dateDaysAgo(500),
    504    },
    505    {
    506      URL: "https://expired2.invalid/",
    507      AccessDateTimeUTC: dateDaysAgo(300),
    508    },
    509    {
    510      URL: "https://expired3.invalid/",
    511      AccessDateTimeUTC: dateDaysAgo(190),
    512    },
    513  ];
    514 
    515  // The following entries should be new enough to migrate.
    516  let unexpiredTypedURLsReferenceItems = [
    517    {
    518      URL: "https://unexpired1.invalid/",
    519      AccessDateTimeUTC: dateDaysAgo(179),
    520    },
    521    {
    522      URL: "https://unexpired2.invalid/",
    523      AccessDateTimeUTC: dateDaysAgo(50),
    524    },
    525    {
    526      URL: "https://unexpired3.invalid/",
    527    },
    528  ];
    529 
    530  let typedURLsReferenceItems = [
    531    ...expiredTypedURLsReferenceItems,
    532    ...unexpiredTypedURLsReferenceItems,
    533  ];
    534 
    535  Assert.less(
    536    MigrationUtils.HISTORY_MAX_AGE_IN_DAYS,
    537    300,
    538    "This test expects the current pref to be less than the youngest expired visit."
    539  );
    540  Assert.greater(
    541    MigrationUtils.HISTORY_MAX_AGE_IN_DAYS,
    542    160,
    543    "This test expects the current pref to be greater than the oldest unexpired visit."
    544  );
    545 
    546  eseDBWritingHelpers.setupDB(
    547    db,
    548    new Map([
    549      [
    550        "Favorites",
    551        {
    552          columns: [
    553            { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
    554            {
    555              type: COLUMN_TYPES.JET_coltypLongText,
    556              name: "Title",
    557              cbMax: 4096,
    558            },
    559            { type: COLUMN_TYPES.JET_coltypLongLong, name: "DateUpdated" },
    560            { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
    561            { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
    562            { type: COLUMN_TYPES.JET_coltypBit, name: "IsFolder" },
    563            { type: COLUMN_TYPES.JET_coltypGUID, name: "ParentId" },
    564          ],
    565          rows: bookmarkReferenceItems,
    566        },
    567      ],
    568      [
    569        "ReadingList",
    570        {
    571          columns: [
    572            { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
    573            {
    574              type: COLUMN_TYPES.JET_coltypLongText,
    575              name: "Title",
    576              cbMax: 4096,
    577            },
    578            { type: COLUMN_TYPES.JET_coltypLongLong, name: "AddedDate" },
    579            { type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId" },
    580            { type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted" },
    581          ],
    582          rows: readingListReferenceItems,
    583        },
    584      ],
    585      [
    586        "TypedURLs",
    587        {
    588          columns: [
    589            { type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096 },
    590            {
    591              type: COLUMN_TYPES.JET_coltypLongLong,
    592              name: "AccessDateTimeUTC",
    593            },
    594          ],
    595          rows: typedURLsReferenceItems,
    596        },
    597      ],
    598    ])
    599  );
    600 
    601  // Manually create an EdgeProfileMigrator rather than going through
    602  // MigrationUtils.getMigrator to avoid the user data availability check, since
    603  // we're mocking out that stuff.
    604  let migrator = new EdgeProfileMigrator();
    605  let bookmarksMigrator = migrator.getBookmarksMigratorForTesting(db);
    606  Assert.ok(bookmarksMigrator.exists, "Should recognize db we just created");
    607 
    608  let seenBookmarks = [];
    609  let listener = events => {
    610    for (let event of events) {
    611      let {
    612        id,
    613        itemType,
    614        url,
    615        title,
    616        dateAdded,
    617        guid,
    618        index,
    619        parentGuid,
    620        parentId,
    621      } = event;
    622      if (title.startsWith("Deleted")) {
    623        ok(false, "Should not see deleted items being bookmarked!");
    624      }
    625      seenBookmarks.push({
    626        id,
    627        parentId,
    628        index,
    629        itemType,
    630        url,
    631        title,
    632        dateAdded,
    633        guid,
    634        parentGuid,
    635      });
    636    }
    637  };
    638  PlacesUtils.observers.addListener(["bookmark-added"], listener);
    639 
    640  let migrateResult = await new Promise(resolve =>
    641    bookmarksMigrator.migrate(resolve)
    642  ).catch(ex => {
    643    console.error(ex);
    644    Assert.ok(false, "Got an exception trying to migrate data! " + ex);
    645    return false;
    646  });
    647  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
    648  Assert.ok(migrateResult, "Migration should succeed");
    649  Assert.equal(
    650    seenBookmarks.length,
    651    5,
    652    "Should have seen 5 items being bookmarked."
    653  );
    654  Assert.equal(
    655    seenBookmarks.length,
    656    MigrationUtils._importQuantities.bookmarks,
    657    "Telemetry should have items"
    658  );
    659 
    660  let menuParents = seenBookmarks.filter(
    661    item => item.parentGuid == PlacesUtils.bookmarks.menuGuid
    662  );
    663  Assert.equal(
    664    menuParents.length,
    665    3,
    666    "Bookmarks are added to the menu without a folder"
    667  );
    668  let toolbarParents = seenBookmarks.filter(
    669    item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid
    670  );
    671  Assert.equal(
    672    toolbarParents.length,
    673    1,
    674    "Should have a single item added to the toolbar"
    675  );
    676  let menuParentGuid = PlacesUtils.bookmarks.menuGuid;
    677  let toolbarParentGuid = PlacesUtils.bookmarks.toolbarGuid;
    678 
    679  let expectedTitlesInMenu = bookmarkReferenceItems
    680    .filter(item => item.ParentId == kEdgeMenuParent)
    681    .map(item => item.Title);
    682  // Hacky, but seems like much the simplest way:
    683  expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
    684  let expectedTitlesInToolbar = bookmarkReferenceItems
    685    .filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf")
    686    .map(item => item.Title);
    687 
    688  for (let bookmark of seenBookmarks) {
    689    let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
    690    let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
    691    if (bookmark.title == "Folder") {
    692      Assert.equal(
    693        bookmark.itemType,
    694        PlacesUtils.bookmarks.TYPE_FOLDER,
    695        "Bookmark " + bookmark.title + " should be a folder"
    696      );
    697    } else {
    698      Assert.notEqual(
    699        bookmark.itemType,
    700        PlacesUtils.bookmarks.TYPE_FOLDER,
    701        "Bookmark " + bookmark.title + " should not be a folder"
    702      );
    703    }
    704 
    705    if (shouldBeInMenu) {
    706      Assert.equal(
    707        bookmark.parentGuid,
    708        menuParentGuid,
    709        "Item '" + bookmark.title + "' should be in menu"
    710      );
    711    } else if (shouldBeInToolbar) {
    712      Assert.equal(
    713        bookmark.parentGuid,
    714        toolbarParentGuid,
    715        "Item '" + bookmark.title + "' should be in toolbar"
    716      );
    717    } else if (
    718      bookmark.guid == menuParentGuid ||
    719      bookmark.guid == toolbarParentGuid
    720    ) {
    721      Assert.ok(
    722        true,
    723        "Expect toolbar and menu folders to not be in menu or toolbar"
    724      );
    725    } else {
    726      // Bit hacky, but we do need to check this.
    727      Assert.equal(
    728        bookmark.title,
    729        "Item in folder",
    730        "Subfoldered item shouldn't be in menu or toolbar"
    731      );
    732      let parent = seenBookmarks.find(
    733        maybeParent => maybeParent.guid == bookmark.parentGuid
    734      );
    735      Assert.equal(
    736        parent && parent.title,
    737        "Folder",
    738        "Subfoldered item should be in subfolder labeled 'Folder'"
    739      );
    740    }
    741 
    742    let dbItem = bookmarkReferenceItems.find(
    743      someItem => bookmark.title == someItem.Title
    744    );
    745    if (!dbItem) {
    746      Assert.ok(
    747        [menuParentGuid, toolbarParentGuid].includes(bookmark.guid),
    748        "This item should be one of the containers"
    749      );
    750    } else {
    751      Assert.equal(dbItem.URL || "", bookmark.url, "URL is correct");
    752      Assert.equal(
    753        dbItem.DateUpdated.valueOf(),
    754        new Date(bookmark.dateAdded).valueOf(),
    755        "Date added is correct"
    756      );
    757    }
    758  }
    759 
    760  MigrationUtils._importQuantities.bookmarks = 0;
    761  seenBookmarks = [];
    762  listener = events => {
    763    for (let event of events) {
    764      let {
    765        id,
    766        itemType,
    767        url,
    768        title,
    769        dateAdded,
    770        guid,
    771        index,
    772        parentGuid,
    773        parentId,
    774      } = event;
    775      seenBookmarks.push({
    776        id,
    777        parentId,
    778        index,
    779        itemType,
    780        url,
    781        title,
    782        dateAdded,
    783        guid,
    784        parentGuid,
    785      });
    786    }
    787  };
    788  PlacesUtils.observers.addListener(["bookmark-added"], listener);
    789 
    790  let readingListMigrator = migrator.getReadingListMigratorForTesting(db);
    791  Assert.ok(readingListMigrator.exists, "Should recognize db we just created");
    792  migrateResult = await new Promise(resolve =>
    793    readingListMigrator.migrate(resolve)
    794  ).catch(ex => {
    795    console.error(ex);
    796    Assert.ok(false, "Got an exception trying to migrate data! " + ex);
    797    return false;
    798  });
    799  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
    800  Assert.ok(migrateResult, "Migration should succeed");
    801  Assert.equal(
    802    seenBookmarks.length,
    803    3,
    804    "Should have seen 3 items being bookmarked (2 items + 1 folder)."
    805  );
    806  Assert.equal(
    807    seenBookmarks.length,
    808    MigrationUtils._importQuantities.bookmarks,
    809    "Telemetry should have items"
    810  );
    811  let readingListContainerLabel = await MigrationUtils.getLocalizedString(
    812    "migration-imported-edge-reading-list"
    813  );
    814 
    815  for (let bookmark of seenBookmarks) {
    816    if (readingListContainerLabel == bookmark.title) {
    817      continue;
    818    }
    819    let referenceItem = readingListReferenceItems.find(
    820      item => item.Title == bookmark.title
    821    );
    822    Assert.ok(referenceItem, "Should have imported what we expected");
    823    Assert.equal(referenceItem.URL, bookmark.url, "Should have the right URL");
    824    readingListReferenceItems.splice(
    825      readingListReferenceItems.findIndex(item => item.Title == bookmark.title),
    826      1
    827    );
    828  }
    829  Assert.ok(
    830    !readingListReferenceItems.length,
    831    "Should have seen all expected items."
    832  );
    833 
    834  let historyDBMigrator = migrator.getHistoryDBMigratorForTesting(db);
    835  await new Promise(resolve => {
    836    historyDBMigrator.migrate(resolve);
    837  });
    838  Assert.ok(true, "History DB migration done!");
    839  for (let expiredEntry of expiredTypedURLsReferenceItems) {
    840    let entry = await PlacesUtils.history.fetch(expiredEntry.URL, {
    841      includeVisits: true,
    842    });
    843    Assert.equal(entry, null, "Should not have found an entry.");
    844  }
    845 
    846  for (let unexpiredEntry of unexpiredTypedURLsReferenceItems) {
    847    let entry = await PlacesUtils.history.fetch(unexpiredEntry.URL, {
    848      includeVisits: true,
    849    });
    850    Assert.equal(entry.url, unexpiredEntry.URL, "Should have the correct URL");
    851    Assert.ok(!!entry.visits.length, "Should have some visits");
    852  }
    853 });