test_history_store.js (16650B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { HistoryEngine } = ChromeUtils.importESModule( 5 "resource://services-sync/engines/history.sys.mjs" 6 ); 7 const { Service } = ChromeUtils.importESModule( 8 "resource://services-sync/service.sys.mjs" 9 ); 10 const { SyncedRecordsTelemetry } = ChromeUtils.importESModule( 11 "resource://services-sync/telemetry.sys.mjs" 12 ); 13 14 const TIMESTAMP1 = (Date.now() - 103406528) * 1000; 15 const TIMESTAMP2 = (Date.now() - 6592903) * 1000; 16 const TIMESTAMP3 = (Date.now() - 123894) * 1000; 17 18 function isDateApproximately(actual, expected, skewMillis = 1000) { 19 let lowerBound = expected - skewMillis; 20 let upperBound = expected + skewMillis; 21 return actual >= lowerBound && actual <= upperBound; 22 } 23 24 let engine, store, fxuri, fxguid, tburi, tbguid; 25 26 async function applyEnsureNoFailures(records) { 27 let countTelemetry = new SyncedRecordsTelemetry(); 28 Assert.equal( 29 (await store.applyIncomingBatch(records, countTelemetry)).length, 30 0 31 ); 32 } 33 34 add_task(async function setup() { 35 engine = new HistoryEngine(Service); 36 await engine.initialize(); 37 store = engine._store; 38 }); 39 40 add_task(async function test_store() { 41 _("Verify that we've got an empty store to work with."); 42 do_check_empty(await store.getAllIDs()); 43 44 _("Let's create an entry in the database."); 45 fxuri = CommonUtils.makeURI("http://getfirefox.com/"); 46 47 await PlacesTestUtils.addVisits({ 48 uri: fxuri, 49 title: "Get Firefox!", 50 visitDate: TIMESTAMP1, 51 }); 52 _("Verify that the entry exists."); 53 let ids = Object.keys(await store.getAllIDs()); 54 Assert.equal(ids.length, 1); 55 fxguid = ids[0]; 56 Assert.ok(await store.itemExists(fxguid)); 57 58 _("If we query a non-existent record, it's marked as deleted."); 59 let record = await store.createRecord("non-existent"); 60 Assert.ok(record.deleted); 61 62 _("Verify createRecord() returns a complete record."); 63 record = await store.createRecord(fxguid); 64 Assert.equal(record.histUri, fxuri.spec); 65 Assert.equal(record.title, "Get Firefox!"); 66 Assert.equal(record.visits.length, 1); 67 Assert.equal(record.visits[0].date, TIMESTAMP1); 68 Assert.equal(record.visits[0].type, Ci.nsINavHistoryService.TRANSITION_LINK); 69 70 _("Let's modify the record and have the store update the database."); 71 let secondvisit = { 72 date: TIMESTAMP2, 73 type: Ci.nsINavHistoryService.TRANSITION_TYPED, 74 }; 75 let onVisitObserved = PlacesTestUtils.waitForNotification(["page-visited"]); 76 let updatedRec = await store.createRecord(fxguid); 77 updatedRec.cleartext.title = "Hol Dir Firefox!"; 78 updatedRec.cleartext.visits.push(secondvisit); 79 await applyEnsureNoFailures([updatedRec]); 80 await onVisitObserved; 81 let queryres = await PlacesUtils.history.fetch(fxuri.spec, { 82 includeVisits: true, 83 }); 84 Assert.equal(queryres.title, "Hol Dir Firefox!"); 85 Assert.deepEqual(queryres.visits, [ 86 { 87 date: new Date(TIMESTAMP2 / 1000), 88 transition: Ci.nsINavHistoryService.TRANSITION_TYPED, 89 }, 90 { 91 date: new Date(TIMESTAMP1 / 1000), 92 transition: Ci.nsINavHistoryService.TRANSITION_LINK, 93 }, 94 ]); 95 await PlacesUtils.history.clear(); 96 }); 97 98 add_task(async function test_store_create() { 99 _("Create a brand new record through the store."); 100 tbguid = Utils.makeGUID(); 101 tburi = CommonUtils.makeURI("http://getthunderbird.com"); 102 let onVisitObserved = PlacesTestUtils.waitForNotification(["page-visited"]); 103 let record = await store.createRecord(tbguid); 104 record.cleartext = { 105 id: tbguid, 106 histUri: tburi.spec, 107 title: "The bird is the word!", 108 visits: [ 109 { date: TIMESTAMP3, type: Ci.nsINavHistoryService.TRANSITION_TYPED }, 110 ], 111 }; 112 await applyEnsureNoFailures([record]); 113 await onVisitObserved; 114 Assert.ok(await store.itemExists(tbguid)); 115 do_check_attribute_count(await store.getAllIDs(), 1); 116 let queryres = await PlacesUtils.history.fetch(tburi.spec, { 117 includeVisits: true, 118 }); 119 Assert.equal(queryres.title, "The bird is the word!"); 120 Assert.deepEqual(queryres.visits, [ 121 { 122 date: new Date(TIMESTAMP3 / 1000), 123 transition: Ci.nsINavHistoryService.TRANSITION_TYPED, 124 }, 125 ]); 126 await PlacesUtils.history.clear(); 127 }); 128 129 add_task(async function test_null_title() { 130 _( 131 "Make sure we handle a null title gracefully (it can happen in some cases, e.g. for resource:// URLs)" 132 ); 133 let resguid = Utils.makeGUID(); 134 let resuri = CommonUtils.makeURI("unknown://title"); 135 let record = await store.createRecord(resguid); 136 record.cleartext = { 137 id: resguid, 138 histUri: resuri.spec, 139 title: null, 140 visits: [ 141 { date: TIMESTAMP3, type: Ci.nsINavHistoryService.TRANSITION_TYPED }, 142 ], 143 }; 144 await applyEnsureNoFailures([record]); 145 do_check_attribute_count(await store.getAllIDs(), 1); 146 147 let queryres = await PlacesUtils.history.fetch(resuri.spec, { 148 includeVisits: true, 149 }); 150 Assert.equal(queryres.title, ""); 151 Assert.deepEqual(queryres.visits, [ 152 { 153 date: new Date(TIMESTAMP3 / 1000), 154 transition: Ci.nsINavHistoryService.TRANSITION_TYPED, 155 }, 156 ]); 157 await PlacesUtils.history.clear(); 158 }); 159 160 add_task(async function test_invalid_records() { 161 _("Make sure we handle invalid URLs in places databases gracefully."); 162 await PlacesUtils.withConnectionWrapper( 163 "test_invalid_record", 164 async function (db) { 165 await db.execute( 166 "INSERT INTO moz_places " + 167 "(url, url_hash, title, rev_host, visit_count, last_visit_date) " + 168 "VALUES ('invalid-uri', hash('invalid-uri'), 'Invalid URI', '.', 1, " + 169 TIMESTAMP3 + 170 ")" 171 ); 172 // Add the corresponding visit to retain database coherence. 173 await db.execute( 174 "INSERT INTO moz_historyvisits " + 175 "(place_id, visit_date, visit_type, session) " + 176 "VALUES ((SELECT id FROM moz_places WHERE url_hash = hash('invalid-uri') AND url = 'invalid-uri'), " + 177 TIMESTAMP3 + 178 ", " + 179 Ci.nsINavHistoryService.TRANSITION_TYPED + 180 ", 1)" 181 ); 182 } 183 ); 184 do_check_attribute_count(await store.getAllIDs(), 1); 185 186 _("Make sure we report records with invalid URIs."); 187 let invalid_uri_guid = Utils.makeGUID(); 188 let countTelemetry = new SyncedRecordsTelemetry(); 189 let failed = await store.applyIncomingBatch( 190 [ 191 { 192 id: invalid_uri_guid, 193 histUri: ":::::::::::::::", 194 title: "Doesn't have a valid URI", 195 visits: [ 196 { date: TIMESTAMP3, type: Ci.nsINavHistoryService.TRANSITION_EMBED }, 197 ], 198 }, 199 ], 200 countTelemetry 201 ); 202 Assert.equal(failed.length, 1); 203 Assert.equal(failed[0], invalid_uri_guid); 204 Assert.equal( 205 countTelemetry.incomingCounts.failedReasons[0].name, 206 "<URL> is not a valid URL." 207 ); 208 Assert.equal(countTelemetry.incomingCounts.failedReasons[0].count, 1); 209 210 _("Make sure we handle records with invalid GUIDs gracefully (ignore)."); 211 await applyEnsureNoFailures([ 212 { 213 id: "invalid", 214 histUri: "http://invalid.guid/", 215 title: "Doesn't have a valid GUID", 216 visits: [ 217 { date: TIMESTAMP3, type: Ci.nsINavHistoryService.TRANSITION_EMBED }, 218 ], 219 }, 220 ]); 221 222 _( 223 "Make sure we handle records with invalid visit codes or visit dates, gracefully ignoring those visits." 224 ); 225 let no_date_visit_guid = Utils.makeGUID(); 226 let no_type_visit_guid = Utils.makeGUID(); 227 let invalid_type_visit_guid = Utils.makeGUID(); 228 let non_integer_visit_guid = Utils.makeGUID(); 229 countTelemetry = new SyncedRecordsTelemetry(); 230 failed = await store.applyIncomingBatch( 231 [ 232 { 233 id: no_date_visit_guid, 234 histUri: "http://no.date.visit/", 235 title: "Visit has no date", 236 visits: [{ type: Ci.nsINavHistoryService.TRANSITION_EMBED }], 237 }, 238 { 239 id: no_type_visit_guid, 240 histUri: "http://no.type.visit/", 241 title: "Visit has no type", 242 visits: [{ date: TIMESTAMP3 }], 243 }, 244 { 245 id: invalid_type_visit_guid, 246 histUri: "http://invalid.type.visit/", 247 title: "Visit has invalid type", 248 visits: [ 249 { 250 date: TIMESTAMP3, 251 type: Ci.nsINavHistoryService.TRANSITION_LINK - 1, 252 }, 253 ], 254 }, 255 { 256 id: non_integer_visit_guid, 257 histUri: "http://non.integer.visit/", 258 title: "Visit has non-integer date", 259 visits: [ 260 { date: 1234.567, type: Ci.nsINavHistoryService.TRANSITION_EMBED }, 261 ], 262 }, 263 ], 264 countTelemetry 265 ); 266 Assert.equal(failed.length, 0); 267 268 // Make sure we can apply tombstones (both valid and invalid) 269 countTelemetry = new SyncedRecordsTelemetry(); 270 failed = await store.applyIncomingBatch( 271 [ 272 { id: no_date_visit_guid, deleted: true }, 273 { id: "not-a-valid-guid", deleted: true }, 274 ], 275 countTelemetry 276 ); 277 Assert.deepEqual(failed, ["not-a-valid-guid"]); 278 Assert.equal( 279 countTelemetry.incomingCounts.failedReasons[0].name, 280 "<URL> is not a valid URL." 281 ); 282 283 _("Make sure we handle records with javascript: URLs gracefully."); 284 await applyEnsureNoFailures( 285 [ 286 { 287 id: Utils.makeGUID(), 288 histUri: "javascript:''", 289 title: "javascript:''", 290 visits: [ 291 { date: TIMESTAMP3, type: Ci.nsINavHistoryService.TRANSITION_EMBED }, 292 ], 293 }, 294 ], 295 countTelemetry 296 ); 297 298 _("Make sure we handle records without any visits gracefully."); 299 await applyEnsureNoFailures([ 300 { 301 id: Utils.makeGUID(), 302 histUri: "http://getfirebug.com", 303 title: "Get Firebug!", 304 visits: [], 305 }, 306 ]); 307 }); 308 309 add_task(async function test_unknowingly_invalid_records() { 310 _("Make sure we handle rejection of records by places gracefully."); 311 let oldCAU = store._canAddURI; 312 store._canAddURI = () => true; 313 try { 314 _("Make sure that when places rejects this record we record it as failed"); 315 let guid = Utils.makeGUID(); 316 let countTelemetry = new SyncedRecordsTelemetry(); 317 let invalidRecord = await store.createRecord(guid); 318 invalidRecord.cleartext = { 319 id: guid, 320 histUri: "javascript:''", 321 title: "javascript:''", 322 visits: [ 323 { 324 date: TIMESTAMP3, 325 type: Ci.nsINavHistoryService.TRANSITION_EMBED, 326 }, 327 ], 328 }; 329 let result = await store.applyIncomingBatch( 330 [invalidRecord], 331 countTelemetry 332 ); 333 deepEqual(result, [guid]); 334 } finally { 335 store._canAddURI = oldCAU; 336 } 337 }); 338 339 add_task(async function test_clamp_visit_dates() { 340 let futureVisitTime = Date.now() + 5 * 60 * 1000; 341 let recentVisitTime = Date.now() - 5 * 60 * 1000; 342 343 let recordA = await store.createRecord("visitAAAAAAA"); 344 recordA.cleartext = { 345 id: "visitAAAAAAA", 346 histUri: "http://example.com/a", 347 title: "A", 348 visits: [ 349 { 350 date: "invalidDate", 351 type: Ci.nsINavHistoryService.TRANSITION_LINK, 352 }, 353 ], 354 }; 355 let recordB = await store.createRecord("visitBBBBBBB"); 356 recordB.cleartext = { 357 id: "visitBBBBBBB", 358 histUri: "http://example.com/b", 359 title: "B", 360 visits: [ 361 { 362 date: 100, 363 type: Ci.nsINavHistoryService.TRANSITION_TYPED, 364 }, 365 { 366 date: 250, 367 type: Ci.nsINavHistoryService.TRANSITION_TYPED, 368 }, 369 { 370 date: recentVisitTime * 1000, 371 type: Ci.nsINavHistoryService.TRANSITION_TYPED, 372 }, 373 ], 374 }; 375 let recordC = await store.createRecord("visitCCCCCCC"); 376 recordC.cleartext = { 377 id: "visitCCCCCCC", 378 histUri: "http://example.com/c", 379 title: "D", 380 visits: [ 381 { 382 date: futureVisitTime * 1000, 383 type: Ci.nsINavHistoryService.TRANSITION_BOOKMARK, 384 }, 385 ], 386 }; 387 let recordD = await store.createRecord("visitDDDDDDD"); 388 recordD.cleartext = { 389 id: "visitDDDDDDD", 390 histUri: "http://example.com/d", 391 title: "D", 392 visits: [ 393 { 394 date: recentVisitTime * 1000, 395 type: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, 396 }, 397 ], 398 }; 399 await applyEnsureNoFailures([recordA, recordB, recordC, recordD]); 400 401 let visitsForA = await PlacesSyncUtils.history.fetchVisitsForURL( 402 "http://example.com/a" 403 ); 404 deepEqual(visitsForA, [], "Should ignore visits with invalid dates"); 405 406 let visitsForB = await PlacesSyncUtils.history.fetchVisitsForURL( 407 "http://example.com/b" 408 ); 409 deepEqual( 410 visitsForB, 411 [ 412 { 413 date: recentVisitTime * 1000, 414 type: Ci.nsINavHistoryService.TRANSITION_TYPED, 415 }, 416 { 417 // We should clamp visit dates older than original Mosaic release. 418 date: PlacesSyncUtils.bookmarks.EARLIEST_BOOKMARK_TIMESTAMP * 1000, 419 type: Ci.nsINavHistoryService.TRANSITION_TYPED, 420 }, 421 ], 422 "Should record clamped visit and valid visit for B" 423 ); 424 425 let visitsForC = await PlacesSyncUtils.history.fetchVisitsForURL( 426 "http://example.com/c" 427 ); 428 equal(visitsForC.length, 1, "Should record clamped future visit for C"); 429 let visitDateForC = PlacesUtils.toDate(visitsForC[0].date); 430 ok( 431 isDateApproximately(visitDateForC, Date.now()), 432 "Should clamp future visit date for C to now" 433 ); 434 435 let visitsForD = await PlacesSyncUtils.history.fetchVisitsForURL( 436 "http://example.com/d" 437 ); 438 deepEqual( 439 visitsForD, 440 [ 441 { 442 date: recentVisitTime * 1000, 443 type: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD, 444 }, 445 ], 446 "Should not clamp valid visit dates" 447 ); 448 }); 449 450 add_task(async function test_remove() { 451 _("Remove an existent record and a non-existent from the store."); 452 await applyEnsureNoFailures([ 453 { id: fxguid, deleted: true }, 454 { id: Utils.makeGUID(), deleted: true }, 455 ]); 456 Assert.equal(false, await store.itemExists(fxguid)); 457 let queryres = await PlacesUtils.history.fetch(fxuri.spec, { 458 includeVisits: true, 459 }); 460 Assert.equal(null, queryres); 461 462 _("Make sure wipe works."); 463 await store.wipe(); 464 do_check_empty(await store.getAllIDs()); 465 queryres = await PlacesUtils.history.fetch(fxuri.spec, { 466 includeVisits: true, 467 }); 468 Assert.equal(null, queryres); 469 queryres = await PlacesUtils.history.fetch(tburi.spec, { 470 includeVisits: true, 471 }); 472 Assert.equal(null, queryres); 473 }); 474 475 add_task(async function test_chunking() { 476 let mvpi = store.MAX_VISITS_PER_INSERT; 477 store.MAX_VISITS_PER_INSERT = 3; 478 let checkChunks = function (input, expected) { 479 let chunks = Array.from(store._generateChunks(input)); 480 deepEqual(chunks, expected); 481 }; 482 try { 483 checkChunks([{ visits: ["x"] }], [[{ visits: ["x"] }]]); 484 485 // 3 should still be one chunk. 486 checkChunks([{ visits: ["x", "x", "x"] }], [[{ visits: ["x", "x", "x"] }]]); 487 488 // 4 should still be one chunk as we don't split individual records. 489 checkChunks( 490 [{ visits: ["x", "x", "x", "x"] }], 491 [[{ visits: ["x", "x", "x", "x"] }]] 492 ); 493 494 // 4 in the first and 1 in the second should be 2 chunks. 495 checkChunks( 496 [{ visits: ["x", "x", "x", "x"] }, { visits: ["x"] }], 497 // expected 498 [[{ visits: ["x", "x", "x", "x"] }], [{ visits: ["x"] }]] 499 ); 500 501 // we put multiple records into chunks 502 checkChunks( 503 [ 504 { visits: ["x", "x"] }, 505 { visits: ["x"] }, 506 { visits: ["x"] }, 507 { visits: ["x", "x"] }, 508 { visits: ["x", "x", "x", "x"] }, 509 ], 510 // expected 511 [ 512 [{ visits: ["x", "x"] }, { visits: ["x"] }], 513 [{ visits: ["x"] }, { visits: ["x", "x"] }], 514 [{ visits: ["x", "x", "x", "x"] }], 515 ] 516 ); 517 } finally { 518 store.MAX_VISITS_PER_INSERT = mvpi; 519 } 520 }); 521 522 add_task(async function test_getAllIDs_filters_file_uris() { 523 let uri = CommonUtils.makeURI("file:///Users/eoger/tps/config.json"); 524 let visitAddedPromise = promiseVisit("added", uri); 525 await PlacesTestUtils.addVisits({ 526 uri, 527 visitDate: Date.now() * 1000, 528 transition: PlacesUtils.history.TRANSITION_LINK, 529 }); 530 await visitAddedPromise; 531 532 do_check_attribute_count(await store.getAllIDs(), 0); 533 534 await PlacesUtils.history.clear(); 535 }); 536 537 add_task(async function test_applyIncomingBatch_filters_file_uris() { 538 const guid = Utils.makeGUID(); 539 let uri = CommonUtils.makeURI("file:///Users/eoger/tps/config.json"); 540 await applyEnsureNoFailures([ 541 { 542 id: guid, 543 histUri: uri.spec, 544 title: "TPS CONFIG", 545 visits: [ 546 { date: TIMESTAMP3, type: Ci.nsINavHistoryService.TRANSITION_TYPED }, 547 ], 548 }, 549 ]); 550 Assert.equal(false, await store.itemExists(guid)); 551 let queryres = await PlacesUtils.history.fetch(uri.spec, { 552 includeVisits: true, 553 }); 554 Assert.equal(null, queryres); 555 }); 556 557 add_task(async function cleanup() { 558 _("Clean up."); 559 await PlacesUtils.history.clear(); 560 });