tor-browser

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

test_history_engine.js (13175B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { Service } = ChromeUtils.importESModule(
      5  "resource://services-sync/service.sys.mjs"
      6 );
      7 const { HistoryEngine } = ChromeUtils.importESModule(
      8  "resource://services-sync/engines/history.sys.mjs"
      9 );
     10 
     11 // Use only for rawAddVisit.
     12 XPCOMUtils.defineLazyServiceGetter(
     13  this,
     14  "asyncHistory",
     15  "@mozilla.org/browser/history;1",
     16  Ci.mozIAsyncHistory
     17 );
     18 async function rawAddVisit(id, uri, visitPRTime, transitionType) {
     19  return new Promise(resolve => {
     20    let results = [];
     21    let handler = {
     22      handleResult(result) {
     23        results.push(result);
     24      },
     25      handleError(resultCode) {
     26        do_throw(`updatePlaces gave error ${resultCode}!`);
     27      },
     28      handleCompletion(count) {
     29        resolve({ results, count });
     30      },
     31    };
     32    asyncHistory.updatePlaces(
     33      [
     34        {
     35          guid: id,
     36          uri: typeof uri == "string" ? CommonUtils.makeURI(uri) : uri,
     37          visits: [{ visitDate: visitPRTime, transitionType }],
     38        },
     39      ],
     40      handler
     41    );
     42  });
     43 }
     44 
     45 add_task(async function test_history_download_limit() {
     46  let engine = new HistoryEngine(Service);
     47  await engine.initialize();
     48 
     49  let server = await serverForFoo(engine);
     50  await SyncTestingInfrastructure(server);
     51 
     52  let lastSync = new_timestamp();
     53 
     54  let collection = server.user("foo").collection("history");
     55  for (let i = 0; i < 15; i++) {
     56    let id = "place" + i.toString(10).padStart(7, "0");
     57    let wbo = new ServerWBO(
     58      id,
     59      encryptPayload({
     60        id,
     61        histUri: "http://example.com/" + i,
     62        title: "Page " + i,
     63        visits: [
     64          {
     65            date: Date.now() * 1000,
     66            type: PlacesUtils.history.TRANSITIONS.TYPED,
     67          },
     68          {
     69            date: Date.now() * 1000,
     70            type: PlacesUtils.history.TRANSITIONS.LINK,
     71          },
     72        ],
     73      }),
     74      lastSync + 1 + i
     75    );
     76    wbo.sortindex = 15 - i;
     77    collection.insertWBO(wbo);
     78  }
     79 
     80  // We have 15 records on the server since the last sync, but our download
     81  // limit is 5 records at a time. We should eventually fetch all 15.
     82  await engine.setLastSync(lastSync);
     83  engine.downloadBatchSize = 4;
     84  engine.downloadLimit = 5;
     85 
     86  // Don't actually fetch any backlogged records, so that we can inspect
     87  // the backlog between syncs.
     88  engine.guidFetchBatchSize = 0;
     89 
     90  let ping = await sync_engine_and_validate_telem(engine, false);
     91  deepEqual(ping.engines[0].incoming, { applied: 5 });
     92 
     93  let backlogAfterFirstSync = Array.from(engine.toFetch).sort();
     94  deepEqual(backlogAfterFirstSync, [
     95    "place0000000",
     96    "place0000001",
     97    "place0000002",
     98    "place0000003",
     99    "place0000004",
    100    "place0000005",
    101    "place0000006",
    102    "place0000007",
    103    "place0000008",
    104    "place0000009",
    105  ]);
    106 
    107  // We should have fast-forwarded the last sync time.
    108  equal(await engine.getLastSync(), lastSync + 15);
    109 
    110  engine.lastModified = collection.modified;
    111  ping = await sync_engine_and_validate_telem(engine, false);
    112  ok(!ping.engines[0].incoming);
    113 
    114  // After the second sync, our backlog still contains the same GUIDs: we
    115  // weren't able to make progress on fetching them, since our
    116  // `guidFetchBatchSize` is 0.
    117  let backlogAfterSecondSync = Array.from(engine.toFetch).sort();
    118  deepEqual(backlogAfterFirstSync, backlogAfterSecondSync);
    119 
    120  // Now add a newer record to the server.
    121  let newWBO = new ServerWBO(
    122    "placeAAAAAAA",
    123    encryptPayload({
    124      id: "placeAAAAAAA",
    125      histUri: "http://example.com/a",
    126      title: "New Page A",
    127      visits: [
    128        {
    129          date: Date.now() * 1000,
    130          type: PlacesUtils.history.TRANSITIONS.TYPED,
    131        },
    132      ],
    133    }),
    134    lastSync + 20
    135  );
    136  newWBO.sortindex = -1;
    137  collection.insertWBO(newWBO);
    138 
    139  engine.lastModified = collection.modified;
    140  ping = await sync_engine_and_validate_telem(engine, false);
    141  deepEqual(ping.engines[0].incoming, { applied: 1 });
    142 
    143  // Our backlog should remain the same.
    144  let backlogAfterThirdSync = Array.from(engine.toFetch).sort();
    145  deepEqual(backlogAfterSecondSync, backlogAfterThirdSync);
    146 
    147  equal(await engine.getLastSync(), lastSync + 20);
    148 
    149  // Bump the fetch batch size to let the backlog make progress. We should
    150  // make 3 requests to fetch 5 backlogged GUIDs.
    151  engine.guidFetchBatchSize = 2;
    152 
    153  engine.lastModified = collection.modified;
    154  ping = await sync_engine_and_validate_telem(engine, false);
    155  deepEqual(ping.engines[0].incoming, { applied: 5 });
    156 
    157  deepEqual(Array.from(engine.toFetch).sort(), [
    158    "place0000005",
    159    "place0000006",
    160    "place0000007",
    161    "place0000008",
    162    "place0000009",
    163  ]);
    164 
    165  // Sync again to clear out the backlog.
    166  engine.lastModified = collection.modified;
    167  ping = await sync_engine_and_validate_telem(engine, false);
    168  deepEqual(ping.engines[0].incoming, { applied: 5 });
    169 
    170  deepEqual(Array.from(engine.toFetch), []);
    171 
    172  await engine.wipeClient();
    173  await engine.finalize();
    174 });
    175 
    176 add_task(async function test_history_visit_roundtrip() {
    177  let engine = new HistoryEngine(Service);
    178  await engine.initialize();
    179  let server = await serverForFoo(engine);
    180  await SyncTestingInfrastructure(server);
    181 
    182  engine._tracker.start();
    183 
    184  let id = "aaaaaaaaaaaa";
    185  let oneHourMS = 60 * 60 * 1000;
    186  // Insert a visit with a non-round microsecond timestamp (e.g. it's not evenly
    187  // divisible by 1000). This will typically be the case for visits that occur
    188  // during normal navigation.
    189  let time = (Date.now() - oneHourMS) * 1000 + 555;
    190  // We use the low level history api since it lets us provide microseconds
    191  let { count } = await rawAddVisit(
    192    id,
    193    "https://www.example.com",
    194    time,
    195    PlacesUtils.history.TRANSITIONS.TYPED
    196  );
    197  equal(count, 1);
    198  // Check that it was inserted and that we didn't round on the insert.
    199  let visits = await PlacesSyncUtils.history.fetchVisitsForURL(
    200    "https://www.example.com"
    201  );
    202  equal(visits.length, 1);
    203  equal(visits[0].date, time);
    204 
    205  let collection = server.user("foo").collection("history");
    206 
    207  // Sync the visit up to the server.
    208  await sync_engine_and_validate_telem(engine, false);
    209 
    210  collection.updateRecord(
    211    id,
    212    cleartext => {
    213      // Double-check that we didn't round the visit's timestamp to the nearest
    214      // millisecond when uploading.
    215      equal(cleartext.visits[0].date, time);
    216      // Add a remote visit so that we get past the deepEquals check in reconcile
    217      // (otherwise the history engine will skip applying this record). The
    218      // contents of this visit don't matter, beyond the fact that it needs to
    219      // exist.
    220      cleartext.visits.push({
    221        date: (Date.now() - oneHourMS / 2) * 1000,
    222        type: PlacesUtils.history.TRANSITIONS.LINK,
    223      });
    224    },
    225    new_timestamp() + 10
    226  );
    227 
    228  // Force a remote sync.
    229  await engine.setLastSync(new_timestamp() - 30);
    230  await sync_engine_and_validate_telem(engine, false);
    231 
    232  // Make sure that we didn't duplicate the visit when inserting. (Prior to bug
    233  // 1423395, we would insert a duplicate visit, where the timestamp was
    234  // effectively `Math.round(microsecondTimestamp / 1000) * 1000`.)
    235  visits = await PlacesSyncUtils.history.fetchVisitsForURL(
    236    "https://www.example.com"
    237  );
    238  equal(visits.length, 2);
    239 
    240  await engine.wipeClient();
    241  await engine.finalize();
    242 });
    243 
    244 add_task(async function test_history_visit_dedupe_old() {
    245  let engine = new HistoryEngine(Service);
    246  await engine.initialize();
    247  let server = await serverForFoo(engine);
    248  await SyncTestingInfrastructure(server);
    249 
    250  let initialVisits = Array.from({ length: 25 }, (_, index) => ({
    251    transition: PlacesUtils.history.TRANSITION_LINK,
    252    date: new Date(Date.UTC(2017, 10, 1 + index)),
    253  }));
    254  initialVisits.push({
    255    transition: PlacesUtils.history.TRANSITION_LINK,
    256    date: new Date(),
    257  });
    258  await PlacesUtils.history.insert({
    259    url: "https://www.example.com",
    260    visits: initialVisits,
    261  });
    262 
    263  let recentVisits = await PlacesSyncUtils.history.fetchVisitsForURL(
    264    "https://www.example.com"
    265  );
    266  equal(recentVisits.length, 20);
    267  let { visits: allVisits, guid } = await PlacesUtils.history.fetch(
    268    "https://www.example.com",
    269    {
    270      includeVisits: true,
    271    }
    272  );
    273  equal(allVisits.length, 26);
    274 
    275  let collection = server.user("foo").collection("history");
    276 
    277  await sync_engine_and_validate_telem(engine, false);
    278 
    279  collection.updateRecord(
    280    guid,
    281    data => {
    282      data.visits.push(
    283        // Add a couple remote visit equivalent to some old visits we have already
    284        {
    285          date: Date.UTC(2017, 10, 1) * 1000, // Nov 1, 2017
    286          type: PlacesUtils.history.TRANSITIONS.LINK,
    287        },
    288        {
    289          date: Date.UTC(2017, 10, 2) * 1000, // Nov 2, 2017
    290          type: PlacesUtils.history.TRANSITIONS.LINK,
    291        },
    292        // Add a couple new visits to make sure we are still applying them.
    293        {
    294          date: Date.UTC(2017, 11, 4) * 1000, // Dec 4, 2017
    295          type: PlacesUtils.history.TRANSITIONS.LINK,
    296        },
    297        {
    298          date: Date.UTC(2017, 11, 5) * 1000, // Dec 5, 2017
    299          type: PlacesUtils.history.TRANSITIONS.LINK,
    300        }
    301      );
    302    },
    303    new_timestamp() + 10
    304  );
    305 
    306  await engine.setLastSync(new_timestamp() - 30);
    307  await sync_engine_and_validate_telem(engine, false);
    308 
    309  allVisits = (
    310    await PlacesUtils.history.fetch("https://www.example.com", {
    311      includeVisits: true,
    312    })
    313  ).visits;
    314 
    315  equal(allVisits.length, 28);
    316  ok(
    317    allVisits.find(x => x.date.getTime() === Date.UTC(2017, 11, 4)),
    318    "Should contain the Dec. 4th visit"
    319  );
    320  ok(
    321    allVisits.find(x => x.date.getTime() === Date.UTC(2017, 11, 5)),
    322    "Should contain the Dec. 5th visit"
    323  );
    324 
    325  await engine.wipeClient();
    326  await engine.finalize();
    327 });
    328 
    329 add_task(async function test_history_unknown_fields() {
    330  let engine = new HistoryEngine(Service);
    331  await engine.initialize();
    332  let server = await serverForFoo(engine);
    333  await SyncTestingInfrastructure(server);
    334 
    335  engine._tracker.start();
    336 
    337  let id = "aaaaaaaaaaaa";
    338  let oneHourMS = 60 * 60 * 1000;
    339  // Insert a visit with a non-round microsecond timestamp (e.g. it's not evenly
    340  // divisible by 1000). This will typically be the case for visits that occur
    341  // during normal navigation.
    342  let time = (Date.now() - oneHourMS) * 1000 + 555;
    343  // We use the low level history api since it lets us provide microseconds
    344  let { count } = await rawAddVisit(
    345    id,
    346    "https://www.example.com",
    347    time,
    348    PlacesUtils.history.TRANSITIONS.TYPED
    349  );
    350  equal(count, 1);
    351 
    352  let collection = server.user("foo").collection("history");
    353 
    354  // Sync the visit up to the server.
    355  await sync_engine_and_validate_telem(engine, false);
    356 
    357  collection.updateRecord(
    358    id,
    359    cleartext => {
    360      equal(cleartext.visits[0].date, time);
    361 
    362      // Add unknown fields to an instance of a visit
    363      cleartext.visits.push({
    364        date: (Date.now() - oneHourMS / 2) * 1000,
    365        type: PlacesUtils.history.TRANSITIONS.LINK,
    366        unknownVisitField: "an unknown field could show up in a visit!",
    367      });
    368      cleartext.title = "A page title";
    369      // Add unknown fields to the payload for this URL
    370      cleartext.unknownStrField = "an unknown str field";
    371      cleartext.unknownObjField = { newField: "a field within an object" };
    372    },
    373    new_timestamp() + 10
    374  );
    375 
    376  // Force a remote sync.
    377  await engine.setLastSync(new_timestamp() - 30);
    378  await sync_engine_and_validate_telem(engine, false);
    379 
    380  // Add a new visit to ensure we're actually putting things back on the server
    381  let newTime = (Date.now() - oneHourMS) * 1000 + 555;
    382  await rawAddVisit(
    383    id,
    384    "https://www.example.com",
    385    newTime,
    386    PlacesUtils.history.TRANSITIONS.LINK
    387  );
    388 
    389  // Sync again
    390  await engine.setLastSync(new_timestamp() - 30);
    391  await sync_engine_and_validate_telem(engine, false);
    392 
    393  let placeInfo = await PlacesSyncUtils.history.fetchURLInfoForGuid(id);
    394 
    395  // Found the place we're looking for
    396  Assert.equal(placeInfo.title, "A page title");
    397  Assert.equal(placeInfo.url, "https://www.example.com/");
    398 
    399  // It correctly returns any unknownFields that might've been
    400  // stored in the moz_places_extra table
    401  deepEqual(JSON.parse(placeInfo.unknownFields), {
    402    unknownStrField: "an unknown str field",
    403    unknownObjField: { newField: "a field within an object" },
    404  });
    405 
    406  // Getting visits via SyncUtils also will return unknownFields
    407  // via the moz_historyvisits_extra table
    408  let visits = await PlacesSyncUtils.history.fetchVisitsForURL(
    409    "https://www.example.com"
    410  );
    411  equal(visits.length, 3);
    412 
    413  // fetchVisitsForURL is a sync method that gets called during upload
    414  // so unknown field should already be at the top-level
    415  deepEqual(
    416    visits[0].unknownVisitField,
    417    "an unknown field could show up in a visit!"
    418  );
    419 
    420  // Remote history record should have the fields back at the top level
    421  let remotePlace = collection.payloads().find(rec => rec.id === id);
    422  deepEqual(remotePlace.unknownStrField, "an unknown str field");
    423  deepEqual(remotePlace.unknownObjField, {
    424    newField: "a field within an object",
    425  });
    426 
    427  await engine.wipeClient();
    428  await engine.finalize();
    429 });