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