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 });