tor-browser

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

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