test_remote_settings.js (55336B)
1 /* import-globals-from ../../../common/tests/unit/head_helpers.js */ 2 3 const { ObjectUtils } = ChromeUtils.importESModule( 4 "resource://gre/modules/ObjectUtils.sys.mjs" 5 ); 6 7 const IS_ANDROID = AppConstants.platform == "android"; 8 9 const TELEMETRY_COMPONENT = "remotesettings"; 10 const TELEMETRY_EVENTS_FILTERS = { 11 category: "uptake.remotecontent.result", 12 method: "uptake", 13 }; 14 15 let server; 16 let client; 17 let clientWithDump; 18 19 async function clear_state() { 20 // Reset preview mode. 21 RemoteSettings.enablePreviewMode(undefined); 22 Services.prefs.clearUserPref("services.settings.preview_enabled"); 23 24 client.verifySignature = false; 25 clientWithDump.verifySignature = false; 26 27 // Clear local DB. 28 await client.db.clear(); 29 // Reset event listeners. 30 client._listeners.set("sync", []); 31 32 await clientWithDump.db.clear(); 33 34 // Clear events snapshot. 35 TelemetryTestUtils.assertEvents([], {}, { process: "dummy" }); 36 } 37 38 add_task(() => { 39 // Set up an HTTP Server 40 server = new HttpServer(); 41 server.start(-1); 42 43 // Point the blocklist clients to use this local HTTP server. 44 Services.prefs.setStringPref( 45 "services.settings.server", 46 `http://localhost:${server.identity.primaryPort}/v1` 47 ); 48 49 Services.prefs.setStringPref("services.settings.loglevel", "debug"); 50 51 client = RemoteSettings("password-fields"); 52 clientWithDump = RemoteSettings("language-dictionaries"); 53 54 server.registerPathHandler("/v1/", handleResponse); 55 server.registerPathHandler( 56 "/v1/buckets/monitor/collections/changes/changeset", 57 handleResponse 58 ); 59 server.registerPathHandler( 60 "/v1/buckets/main/collections/password-fields/changeset", 61 handleResponse 62 ); 63 server.registerPathHandler( 64 "/v1/buckets/main/collections/language-dictionaries/changeset", 65 handleResponse 66 ); 67 server.registerPathHandler( 68 "/v1/buckets/main/collections/with-local-fields/changeset", 69 handleResponse 70 ); 71 server.registerPathHandler("/fake-x5u", handleResponse); 72 73 registerCleanupFunction(() => { 74 server.stop(() => {}); 75 }); 76 }); 77 add_task(clear_state); 78 79 add_task(async function test_records_obtained_from_server_are_stored_in_db() { 80 // Test an empty db populates 81 await client.maybeSync(2000); 82 83 // Open the collection, verify it's been populated: 84 // Our test data has a single record; it should be in the local collection 85 const list = await client.get(); 86 equal(list.length, 1); 87 88 const timestamp = await client.db.getLastModified(); 89 equal(timestamp, 3000, "timestamp was stored"); 90 91 const { signatures } = await client.db.getMetadata(); 92 equal(signatures[0].signature, "abcdef", "metadata was stored"); 93 }); 94 add_task(clear_state); 95 96 add_task(async function test_client_db_throws_if_not_synced() { 97 try { 98 await client.db.list(); 99 Assert.ok(false, "db.list() should throw"); 100 } catch (e) { 101 Assert.equal( 102 e.toString(), 103 'EmptyDatabaseError: "main/password-fields" has not been synced yet' 104 ); 105 } 106 107 await client.maybeSync(2000); 108 109 const list = await client.db.list(); 110 Assert.ok(Array.isArray(list), "data is an array"); 111 }); 112 add_task(clear_state); 113 114 add_task( 115 async function test_records_from_dump_are_listed_as_created_in_event() { 116 if (IS_ANDROID) { 117 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 118 return; 119 } 120 let received; 121 clientWithDump.on("sync", ({ data }) => (received = data)); 122 // Use a timestamp superior to latest record in dump. 123 const timestamp = 5000000000000; // Fri Jun 11 2128 124 125 await clientWithDump.maybeSync(timestamp); 126 127 const list = await clientWithDump.get(); 128 Assert.greater( 129 list.length, 130 20, 131 `The dump was loaded (${list.length} records)` 132 ); 133 equal(received.created[0].id, "xx", "Record from the sync come first."); 134 135 const createdById = received.created.reduce((acc, r) => { 136 acc[r.id] = r; 137 return acc; 138 }, {}); 139 140 ok( 141 !(received.deleted[0].id in createdById), 142 "Deleted records are not listed as created" 143 ); 144 equal( 145 createdById[received.updated[0].new.id], 146 received.updated[0].new, 147 "The records that were updated should appear as created in their newest form." 148 ); 149 150 equal( 151 received.created.length, 152 list.length, 153 "The list of created records contains the dump" 154 ); 155 equal(received.current.length, received.created.length); 156 } 157 ); 158 add_task(clear_state); 159 160 add_task(async function test_throws_when_network_is_offline() { 161 const backupOffline = Services.io.offline; 162 try { 163 Services.io.offline = true; 164 const startSnapshot = getUptakeTelemetrySnapshot( 165 TELEMETRY_COMPONENT, 166 clientWithDump.identifier 167 ); 168 let error; 169 try { 170 await clientWithDump.maybeSync(2000); 171 } catch (e) { 172 error = e; 173 } 174 equal(error.name, "NetworkOfflineError"); 175 176 const endSnapshot = getUptakeTelemetrySnapshot( 177 TELEMETRY_COMPONENT, 178 clientWithDump.identifier 179 ); 180 const expectedIncrements = { 181 [UptakeTelemetry.STATUS.SYNC_START]: 1, 182 [UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR]: 1, 183 }; 184 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 185 } finally { 186 Services.io.offline = backupOffline; 187 } 188 }); 189 add_task(clear_state); 190 191 add_task(async function test_sync_event_is_sent_even_if_up_to_date() { 192 if (IS_ANDROID) { 193 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 194 return; 195 } 196 // First, determine what is the dump timestamp. Sync will load it. 197 // Use a timestamp inferior to latest record in dump. 198 await clientWithDump._importJSONDump(); 199 const uptodateTimestamp = await clientWithDump.db.getLastModified(); 200 await clear_state(); 201 202 // Now, simulate that server data wasn't changed since dump was released. 203 const startSnapshot = getUptakeTelemetrySnapshot( 204 TELEMETRY_COMPONENT, 205 clientWithDump.identifier 206 ); 207 let received; 208 clientWithDump.on("sync", ({ data }) => (received = data)); 209 210 await clientWithDump.maybeSync(uptodateTimestamp); 211 212 ok(!!received.current.length, "Dump records are listed as created"); 213 equal(received.current.length, received.created.length); 214 215 const endSnapshot = getUptakeTelemetrySnapshot( 216 TELEMETRY_COMPONENT, 217 clientWithDump.identifier 218 ); 219 const expectedIncrements = { 220 [UptakeTelemetry.STATUS.SYNC_START]: 1, 221 [UptakeTelemetry.STATUS.UP_TO_DATE]: 1, 222 }; 223 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 224 }); 225 add_task(clear_state); 226 227 add_task(async function test_records_can_have_local_fields() { 228 const c = RemoteSettings("with-local-fields", { localFields: ["accepted"] }); 229 c.verifySignature = false; 230 231 await c.maybeSync(2000); 232 233 await c.db.update({ 234 id: "c74279ce-fb0a-42a6-ae11-386b567a6119", 235 accepted: true, 236 }); 237 await c.maybeSync(3000); // Does not fail. 238 }); 239 add_task(clear_state); 240 241 add_task( 242 async function test_records_changes_are_overwritten_by_server_changes() { 243 // Create some local conflicting data, and make sure it syncs without error. 244 await client.db.create({ 245 website: "", 246 id: "9d500963-d80e-3a91-6e74-66f3811b99cc", 247 }); 248 249 await client.maybeSync(2000); 250 251 const data = await client.get(); 252 equal(data[0].website, "https://some-website.com"); 253 } 254 ); 255 add_task(clear_state); 256 257 add_task( 258 async function test_get_returns_an_empty_list_when_database_is_empty() { 259 const data = await client.get({ syncIfEmpty: false }); 260 261 ok(Array.isArray(data), "data is an array"); 262 equal(data.length, 0, "data is empty"); 263 } 264 ); 265 add_task(clear_state); 266 267 add_task(async function test_get_doesnt_affect_other_calls() { 268 const c1 = RemoteSettings("password-fields"); 269 const c2 = RemoteSettings("password-fields"); 270 271 const result1 = await c1.get({ syncIfEmpty: false }); 272 Assert.deepEqual(result1, [], "data1 is empty"); 273 274 try { 275 await c2.get({ syncIfEmpty: false, emptyListFallback: false }); 276 Assert.ok(false, "get() should throw"); 277 } catch (error) { 278 Assert.equal( 279 error.toString(), 280 'EmptyDatabaseError: "main/password-fields" has not been synced yet' 281 ); 282 } 283 }); 284 add_task(clear_state); 285 286 add_task(async function test_get_throws_if_no_empty_fallback_and_no_sync() { 287 try { 288 await client.get({ syncIfEmpty: false, emptyListFallback: false }); 289 Assert.ok(false, ".get() should throw"); 290 } catch (error) { 291 Assert.equal( 292 error.toString(), 293 'EmptyDatabaseError: "main/password-fields" has not been synced yet' 294 ); 295 } 296 }); 297 add_task(clear_state); 298 299 add_task( 300 async function test_get_loads_default_records_from_a_local_dump_when_database_is_empty() { 301 if (IS_ANDROID) { 302 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 303 return; 304 } 305 306 // When collection has a dump in services/settings/dumps/{bucket}/{collection}.json 307 const data = await clientWithDump.get(); 308 notEqual(data.length, 0); 309 // No synchronization happened (responses are not mocked). 310 } 311 ); 312 add_task(clear_state); 313 314 add_task(async function test_get_loads_dump_only_once_if_called_in_parallel() { 315 const backup = clientWithDump._importJSONDump; 316 let callCount = 0; 317 clientWithDump._importJSONDump = async () => { 318 callCount++; 319 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 320 await new Promise(resolve => setTimeout(resolve, 100)); 321 return 42; 322 }; 323 await Promise.all([clientWithDump.get(), clientWithDump.get()]); 324 equal(callCount, 1, "JSON dump was called more than once"); 325 clientWithDump._importJSONDump = backup; 326 }); 327 add_task(clear_state); 328 329 add_task(async function test_get_falls_back_to_dump_if_db_fails() { 330 if (IS_ANDROID) { 331 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 332 return; 333 } 334 const backup = clientWithDump.db.getLastModified; 335 clientWithDump.db.getLastModified = () => { 336 throw new Error("Unknown error"); 337 }; 338 339 const records = await clientWithDump.get({ dumpFallback: true }); 340 ok(!!records.length, "dump content is returned"); 341 342 // If fallback is disabled, error is thrown. 343 let error; 344 try { 345 await clientWithDump.get({ dumpFallback: false }); 346 } catch (e) { 347 error = e; 348 } 349 equal(error.message, "Unknown error"); 350 351 clientWithDump.db.getLastModified = backup; 352 }); 353 add_task(clear_state); 354 355 add_task(async function test_get_sorts_results_if_specified() { 356 await client.db.importChanges( 357 {}, 358 42, 359 [ 360 { 361 field: 12, 362 id: "9d500963-d80e-3a91-6e74-66f3811b99cc", 363 }, 364 { 365 field: 7, 366 id: "d83444a4-f348-4cd8-8228-842cb927db9f", 367 }, 368 ], 369 { clear: true } 370 ); 371 372 const records = await client.get({ order: "field" }); 373 Assert.less( 374 records[0].field, 375 records[records.length - 1].field, 376 "records are sorted" 377 ); 378 }); 379 add_task(clear_state); 380 381 add_task(async function test_get_falls_back_sorts_results() { 382 if (IS_ANDROID) { 383 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 384 return; 385 } 386 const backup = clientWithDump.db.getLastModified; 387 clientWithDump.db.getLastModified = () => { 388 throw new Error("Unknown error"); 389 }; 390 391 const records = await clientWithDump.get({ 392 dumpFallback: true, 393 order: "-id", 394 }); 395 396 // eslint-disable-next-line mozilla/no-comparison-or-assignment-inside-ok 397 ok(records[0].id > records[records.length - 1].id, "records are sorted"); 398 399 clientWithDump.db.getLastModified = backup; 400 }); 401 add_task(clear_state); 402 403 add_task(async function test_get_falls_back_to_dump_if_db_fails_later() { 404 if (IS_ANDROID) { 405 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 406 return; 407 } 408 const backup = clientWithDump.db.list; 409 clientWithDump.db.list = () => { 410 throw new Error("Unknown error"); 411 }; 412 413 const records = await clientWithDump.get({ dumpFallback: true }); 414 ok(!!records.length, "dump content is returned"); 415 416 // If fallback is disabled, error is thrown. 417 let error; 418 try { 419 await clientWithDump.get({ dumpFallback: false }); 420 } catch (e) { 421 error = e; 422 } 423 equal(error.message, "Unknown error"); 424 425 clientWithDump.db.list = backup; 426 }); 427 add_task(clear_state); 428 429 add_task(async function test_get_falls_back_to_dump_if_network_fails() { 430 if (IS_ANDROID) { 431 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 432 return; 433 } 434 const backup = clientWithDump.sync; 435 clientWithDump.sync = () => { 436 throw new Error("Sync error"); 437 }; 438 439 const records = await clientWithDump.get(); 440 ok(!!records.length, "dump content is returned"); 441 442 clientWithDump.sync = backup; 443 }); 444 add_task(clear_state); 445 446 add_task(async function test_get_does_not_sync_if_empty_dump_is_provided() { 447 if (IS_ANDROID) { 448 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 449 return; 450 } 451 452 const clientWithEmptyDump = RemoteSettings("example"); 453 Assert.ok(!(await Utils.hasLocalData(clientWithEmptyDump))); 454 455 const data = await clientWithEmptyDump.get(); 456 457 equal(data.length, 0); 458 Assert.ok(await Utils.hasLocalData(clientWithEmptyDump)); 459 }); 460 add_task(clear_state); 461 462 add_task(async function test_get_synchronization_can_be_disabled() { 463 const data = await client.get({ syncIfEmpty: false }); 464 465 equal(data.length, 0); 466 }); 467 add_task(clear_state); 468 469 add_task( 470 async function test_get_triggers_synchronization_when_database_is_empty() { 471 // The "password-fields" collection has no local dump, and no local data. 472 // Therefore a synchronization will happen. 473 const data = await client.get(); 474 475 // Data comes from mocked HTTP response (see below). 476 equal(data.length, 1); 477 equal(data[0].selector, "#webpage[field-pwd]"); 478 } 479 ); 480 add_task(clear_state); 481 482 add_task(async function test_get_ignores_synchronization_errors_by_default() { 483 // The monitor endpoint won't contain any information about this collection. 484 let data = await RemoteSettings("some-unknown-key").get(); 485 equal(data.length, 0); 486 // The sync endpoints are not mocked, this fails internally. 487 data = await RemoteSettings("no-mocked-responses").get(); 488 equal(data.length, 0); 489 }); 490 add_task(clear_state); 491 492 add_task(async function test_get_throws_if_no_empty_fallback() { 493 // The monitor endpoint won't contain any information about this collection. 494 try { 495 await RemoteSettings("some-unknown-key").get({ 496 emptyListFallback: false, 497 }); 498 Assert.ok(false, ".get() should throw"); 499 } catch (error) { 500 Assert.ok( 501 error.message.includes("Response from server unparseable"), 502 "Server error was thrown" 503 ); 504 } 505 }); 506 add_task(clear_state); 507 508 add_task( 509 async function test_get_throws_on_network_error_with_no_empty_fallback() { 510 const backup = Utils.fetch; 511 Utils.fetch = async () => { 512 throw new Error("Fake Network error"); 513 }; 514 515 const clientEmpty = RemoteSettings("no-dump-no-local-data"); 516 let error; 517 try { 518 await clientEmpty.get({ 519 emptyListFallback: false, 520 syncIfEmpty: true, // default value 521 }); 522 } catch (exc) { 523 error = exc; 524 } 525 526 equal(error.toString(), "Error: Fake Network error"); 527 Utils.fetch = backup; 528 } 529 ); 530 531 add_task(async function test_get_verify_signature_empty_no_sync() { 532 client.verifySignature = true; 533 // No data, hence no signature in metadata, and no sync if empty. 534 let error; 535 try { 536 await client.get({ 537 verifySignature: true, 538 syncIfEmpty: false, 539 emptyListFallback: false, 540 }); 541 Assert.ok(false, "get() should throw"); 542 } catch (e) { 543 error = e; 544 } 545 equal( 546 error.toString(), 547 'EmptyDatabaseError: "main/password-fields" has not been synced yet' 548 ); 549 }); 550 add_task(clear_state); 551 552 add_task(async function test_get_verify_signature_no_metadata_no_sync() { 553 client.verifySignature = true; 554 // Store records but no metadata. 555 await client.db.importChanges(undefined, 42, []); 556 let error; 557 try { 558 await client.get({ 559 verifySignature: true, 560 syncIfEmpty: false, 561 emptyListFallback: false, 562 }); 563 Assert.ok(false, "get() should throw"); 564 } catch (e) { 565 error = e; 566 } 567 equal( 568 error.toString(), 569 "MissingSignatureError: Missing signature (main/password-fields)" 570 ); 571 }); 572 add_task(clear_state); 573 574 add_task(async function test_get_can_verify_signature_pulled() { 575 // Populate the local DB (only records, eg. loaded from dump previously) 576 await client._importJSONDump(); 577 578 let calledSignature; 579 client._verifier = { 580 async asyncVerifyContentSignature(serialized, signature) { 581 calledSignature = signature; 582 return true; 583 }, 584 }; 585 client.verifySignature = true; 586 587 // No metadata in local DB, but gets pulled and then verifies. 588 ok(ObjectUtils.isEmpty(await client.db.getMetadata()), "Metadata is empty"); 589 590 await client.get({ verifySignature: true }); 591 592 ok( 593 !ObjectUtils.isEmpty(await client.db.getMetadata()), 594 "Metadata was pulled" 595 ); 596 ok(calledSignature.endsWith("some-sig"), "Signature was verified"); 597 }); 598 add_task(clear_state); 599 600 add_task(async function test_get_can_verify_signature() { 601 // Populate the local DB (record and metadata) 602 await client.maybeSync(2000); 603 604 client.verifySignature = true; 605 606 // It validates signature that was stored in local DB. 607 let calledSignature; 608 client._verifier = { 609 async asyncVerifyContentSignature(serialized, signature) { 610 calledSignature = signature; 611 return JSON.parse(serialized).data.length == 1; 612 }, 613 }; 614 ok(await Utils.hasLocalData(client), "Local data was populated"); 615 await client.get({ verifySignature: true }); 616 617 ok(calledSignature.endsWith("abcdef"), "Signature was verified"); 618 619 // It throws when signature does not verify. 620 await client.db.delete("9d500963-d80e-3a91-6e74-66f3811b99cc"); 621 let error = null; 622 try { 623 await client.get({ verifySignature: true }); 624 } catch (e) { 625 error = e; 626 } 627 equal( 628 error.message, 629 "Invalid content signature (main/password-fields) using 'fake-x5u' and signer remote-settings.content-signature.mozilla.org" 630 ); 631 }); 632 add_task(clear_state); 633 634 add_task(async function test_get_does_not_verify_signature_if_load_dump() { 635 if (IS_ANDROID) { 636 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 637 return; 638 } 639 640 let called; 641 clientWithDump._verifier = { 642 async asyncVerifyContentSignature() { 643 called = true; 644 return true; 645 }, 646 }; 647 648 // When dump is loaded, signature is not verified. 649 const records = await clientWithDump.get({ verifySignature: true }); 650 ok(!!records.length, "dump is loaded"); 651 ok(!called, "signature is missing but not verified"); 652 653 // If metadata is missing locally, it is not fetched if `syncIfEmpty` is disabled. 654 clientWithDump.verifySignature = true; 655 let error; 656 try { 657 await clientWithDump.get({ verifySignature: true, syncIfEmpty: false }); 658 } catch (e) { 659 error = e; 660 } 661 ok(!called, "signer was not called"); 662 equal( 663 error.message, 664 "Missing signature (main/language-dictionaries)", 665 "signature is missing locally" 666 ); 667 668 // If metadata is missing locally, it is fetched by default (`syncIfEmpty: true`) 669 await clientWithDump.get({ verifySignature: true }); 670 const metadata = await clientWithDump.db.getMetadata(); 671 ok(!!Object.keys(metadata).length, "metadata was fetched"); 672 ok(called, "signature was verified for the data that was in dump"); 673 clientWithDump.verifySignature = true; 674 }); 675 add_task(clear_state); 676 677 add_task( 678 async function test_get_does_verify_signature_if_json_loaded_in_parallel() { 679 const backup = clientWithDump._verifier; 680 let callCount = 0; 681 clientWithDump._verifier = { 682 async asyncVerifyContentSignature() { 683 callCount++; 684 return true; 685 }, 686 }; 687 await Promise.all([ 688 clientWithDump.get({ verifySignature: true }), 689 clientWithDump.get({ verifySignature: true }), 690 ]); 691 equal(callCount, 0, "No need to verify signatures if JSON dump is loaded"); 692 clientWithDump._verifier = backup; 693 } 694 ); 695 add_task(clear_state); 696 697 add_task(async function test_get_can_force_a_sync() { 698 const step0 = await client.db.getLastModified(); 699 await client.get({ forceSync: true }); 700 const step1 = await client.db.getLastModified(); 701 await client.get(); 702 const step2 = await client.db.getLastModified(); 703 await client.get({ forceSync: true }); 704 const step3 = await client.db.getLastModified(); 705 706 equal(step0, null); 707 equal(step1, 3000); 708 equal(step2, 3000); 709 equal(step3, 3001); 710 }); 711 add_task(clear_state); 712 713 add_task(async function test_sync_runs_once_only() { 714 const backup = Utils.log.warn; 715 const messages = []; 716 Utils.log.warn = m => { 717 messages.push(m); 718 }; 719 720 await Promise.all([client.maybeSync(2000), client.maybeSync(2000)]); 721 722 ok( 723 messages.includes("main/password-fields sync already running"), 724 "warning is shown about sync already running" 725 ); 726 Utils.log.warn = backup; 727 }); 728 add_task(clear_state); 729 730 add_task( 731 async function test_sync_pulls_metadata_if_missing_with_dump_is_up_to_date() { 732 if (IS_ANDROID) { 733 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 734 return; 735 } 736 737 let called; 738 clientWithDump._verifier = { 739 async asyncVerifyContentSignature() { 740 called = true; 741 return true; 742 }, 743 }; 744 // When dump is loaded, signature is not verified. 745 const records = await clientWithDump.get({ verifySignature: true }); 746 ok(!!records.length, "dump is loaded"); 747 ok(!called, "signature is missing but not verified"); 748 749 // Synchronize the collection (local data is up-to-date). 750 // Signature verification is disabled (see `clear_state()`), so we don't bother with 751 // fetching metadata. 752 const uptodateTimestamp = await clientWithDump.db.getLastModified(); 753 await clientWithDump.maybeSync(uptodateTimestamp); 754 let metadata = await clientWithDump.db.getMetadata(); 755 ok(!metadata, "metadata was not fetched"); 756 757 // Synchronize again the collection (up-to-date, since collection last modified still > 42) 758 clientWithDump.verifySignature = true; 759 await clientWithDump.maybeSync(42); 760 761 // With signature verification, metadata was fetched. 762 metadata = await clientWithDump.db.getMetadata(); 763 ok(!!Object.keys(metadata).length, "metadata was fetched"); 764 ok(called, "signature was verified for the data that was in dump"); 765 766 // Metadata is present, signature will now verified. 767 called = false; 768 await clientWithDump.get({ verifySignature: true }); 769 ok(called, "local signature is verified"); 770 } 771 ); 772 add_task(clear_state); 773 774 add_task(async function test_sync_event_provides_information_about_records() { 775 let eventData; 776 client.on("sync", ({ data }) => (eventData = data)); 777 778 await client.maybeSync(2000); 779 equal(eventData.current.length, 1); 780 781 await client.maybeSync(3001); 782 equal(eventData.current.length, 2); 783 equal(eventData.created.length, 1); 784 equal(eventData.created[0].website, "https://www.other.org/signin"); 785 equal(eventData.updated.length, 1); 786 equal(eventData.updated[0].old.website, "https://some-website.com"); 787 equal(eventData.updated[0].new.website, "https://some-website.com/login"); 788 equal(eventData.deleted.length, 0); 789 790 await client.maybeSync(4001); 791 equal(eventData.current.length, 1); 792 equal(eventData.created.length, 0); 793 equal(eventData.updated.length, 0); 794 equal(eventData.deleted.length, 1); 795 equal(eventData.deleted[0].website, "https://www.other.org/signin"); 796 }); 797 add_task(clear_state); 798 799 add_task(async function test_inspect_method() { 800 // Synchronize the `password-fields` collection in order to have 801 // some local data when .inspect() is called. 802 await client.maybeSync(2000); 803 804 const inspected = await RemoteSettings.inspect(); 805 806 // Assertion for global attributes. 807 const { mainBucket, serverURL, defaultSigner, collections, serverTimestamp } = 808 inspected; 809 const rsSigner = "remote-settings.content-signature.mozilla.org"; 810 equal(mainBucket, "main"); 811 equal(serverURL, `http://localhost:${server.identity.primaryPort}/v1`); 812 equal(defaultSigner, rsSigner); 813 equal(serverTimestamp, '"5000"'); 814 815 // A collection is listed in .inspect() if it has local data or if there 816 // is a JSON dump for it. 817 // "password-fields" has no dump but was synchronized above and thus has local data. 818 let col = collections.pop(); 819 equal(col.collection, "password-fields"); 820 equal(col.serverTimestamp, 3000); 821 equal(col.localTimestamp, 3000); 822 823 if (!IS_ANDROID) { 824 // "language-dictionaries" has a local dump (not on Android) 825 col = collections.pop(); 826 equal(col.collection, "language-dictionaries"); 827 equal(col.serverTimestamp, 4000); 828 ok(!col.localTimestamp); // not synchronized. 829 } 830 }); 831 add_task(clear_state); 832 833 add_task(async function test_inspect_method_uses_a_random_cache_bust() { 834 const backup = Utils.fetchLatestChanges; 835 const cacheBusts = []; 836 Utils.fetchLatestChanges = (url, options) => { 837 cacheBusts.push(options.expected); 838 return { changes: [] }; 839 }; 840 841 await RemoteSettings.inspect(); 842 await RemoteSettings.inspect(); 843 await RemoteSettings.inspect(); 844 845 notEqual(cacheBusts[0], cacheBusts[1]); 846 notEqual(cacheBusts[1], cacheBusts[2]); 847 notEqual(cacheBusts[0], cacheBusts[2]); 848 Utils.fetchLatestChanges = backup; 849 }); 850 851 add_task(async function test_jexl_context_is_shown_in_inspect() { 852 const { jexlContext } = await RemoteSettings.inspect(); 853 deepEqual(Object.keys(jexlContext).sort(), [ 854 "appinfo", 855 "channel", 856 "country", 857 "formFactor", 858 "locale", 859 "os", 860 "version", 861 ]); 862 deepEqual(Object.keys(jexlContext.os).sort(), ["name", "version"]); 863 deepEqual(Object.keys(jexlContext.appinfo).sort(), ["ID", "OS"]); 864 }); 865 866 add_task(async function test_clearAll_method() { 867 // Make sure we have some local data. 868 await client.maybeSync(2000); 869 await clientWithDump.maybeSync(2000); 870 871 await RemoteSettings.clearAll(); 872 873 ok(!(await Utils.hasLocalData(client)), "Local data was deleted"); 874 ok(!(await Utils.hasLocalData(clientWithDump)), "Local data was deleted"); 875 ok( 876 !Services.prefs.prefHasUserValue(client.lastCheckTimePref), 877 "Pref was cleaned" 878 ); 879 880 // Synchronization is not broken after resuming. 881 await client.maybeSync(2000); 882 await clientWithDump.maybeSync(2000); 883 ok(await Utils.hasLocalData(client), "Local data was populated"); 884 ok(await Utils.hasLocalData(clientWithDump), "Local data was populated"); 885 }); 886 add_task(clear_state); 887 888 add_task(async function test_listeners_are_not_deduplicated() { 889 let count = 0; 890 const plus1 = () => { 891 count += 1; 892 }; 893 894 client.on("sync", plus1); 895 client.on("sync", plus1); 896 client.on("sync", plus1); 897 898 await client.maybeSync(2000); 899 900 equal(count, 3); 901 }); 902 add_task(clear_state); 903 904 add_task(async function test_listeners_can_be_removed() { 905 let count = 0; 906 const onSync = () => { 907 count += 1; 908 }; 909 910 client.on("sync", onSync); 911 client.off("sync", onSync); 912 913 await client.maybeSync(2000); 914 915 equal(count, 0); 916 }); 917 add_task(clear_state); 918 919 add_task(async function test_all_listeners_are_executed_if_one_fails() { 920 let count = 0; 921 client.on("sync", () => { 922 count += 1; 923 }); 924 client.on("sync", () => { 925 throw new Error("boom"); 926 }); 927 client.on("sync", () => { 928 count += 2; 929 }); 930 931 let error; 932 try { 933 await client.maybeSync(2000); 934 } catch (e) { 935 error = e; 936 } 937 938 equal(count, 3); 939 equal(error.message, "boom"); 940 }); 941 add_task(clear_state); 942 943 add_task(async function test_telemetry_reports_up_to_date() { 944 await client.maybeSync(2000); 945 const startSnapshot = getUptakeTelemetrySnapshot( 946 TELEMETRY_COMPONENT, 947 client.identifier 948 ); 949 950 await client.maybeSync(3000); 951 952 // No Telemetry was sent. 953 const endSnapshot = getUptakeTelemetrySnapshot( 954 TELEMETRY_COMPONENT, 955 client.identifier 956 ); 957 const expectedIncrements = { [UptakeTelemetry.STATUS.UP_TO_DATE]: 1 }; 958 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 959 }); 960 add_task(clear_state); 961 962 add_task(async function test_telemetry_if_sync_succeeds() { 963 // We test each client because Telemetry requires preleminary declarations. 964 const startSnapshot = getUptakeTelemetrySnapshot( 965 TELEMETRY_COMPONENT, 966 client.identifier 967 ); 968 969 await client.maybeSync(2000); 970 971 const endSnapshot = getUptakeTelemetrySnapshot( 972 TELEMETRY_COMPONENT, 973 client.identifier 974 ); 975 const expectedIncrements = { 976 [UptakeTelemetry.STATUS.SYNC_START]: 1, 977 [UptakeTelemetry.STATUS.SUCCESS]: 1, 978 }; 979 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 980 }); 981 add_task(clear_state); 982 983 add_task( 984 async function test_synchronization_duration_is_reported_in_uptake_status() { 985 await client.maybeSync(2000); 986 987 TelemetryTestUtils.assertEvents( 988 [ 989 [ 990 "uptake.remotecontent.result", 991 "uptake", 992 "remotesettings", 993 UptakeTelemetry.STATUS.SYNC_START, 994 { 995 source: client.identifier, 996 trigger: "manual", 997 }, 998 ], 999 [ 1000 "uptake.remotecontent.result", 1001 "uptake", 1002 "remotesettings", 1003 UptakeTelemetry.STATUS.SUCCESS, 1004 { 1005 source: client.identifier, 1006 duration: v => v > 0, 1007 trigger: "manual", 1008 }, 1009 ], 1010 ], 1011 TELEMETRY_EVENTS_FILTERS 1012 ); 1013 } 1014 ); 1015 add_task(clear_state); 1016 1017 add_task(async function test_telemetry_reports_if_application_fails() { 1018 const startSnapshot = getUptakeTelemetrySnapshot( 1019 TELEMETRY_COMPONENT, 1020 client.identifier 1021 ); 1022 client.on("sync", () => { 1023 throw new Error("boom"); 1024 }); 1025 1026 try { 1027 await client.maybeSync(2000); 1028 } catch (e) {} 1029 1030 const endSnapshot = getUptakeTelemetrySnapshot( 1031 TELEMETRY_COMPONENT, 1032 client.identifier 1033 ); 1034 const expectedIncrements = { 1035 [UptakeTelemetry.STATUS.SYNC_START]: 1, 1036 [UptakeTelemetry.STATUS.APPLY_ERROR]: 1, 1037 }; 1038 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 1039 }); 1040 add_task(clear_state); 1041 1042 add_task(async function test_telemetry_reports_if_sync_fails() { 1043 await client.db.importChanges({}, 9999); 1044 1045 const startSnapshot = getUptakeTelemetrySnapshot( 1046 TELEMETRY_COMPONENT, 1047 client.identifier 1048 ); 1049 1050 try { 1051 await client.maybeSync(10000); 1052 } catch (e) {} 1053 1054 const endSnapshot = getUptakeTelemetrySnapshot( 1055 TELEMETRY_COMPONENT, 1056 client.identifier 1057 ); 1058 const expectedIncrements = { 1059 [UptakeTelemetry.STATUS.SYNC_START]: 1, 1060 [UptakeTelemetry.STATUS.SERVER_ERROR]: 1, 1061 }; 1062 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 1063 }); 1064 add_task(clear_state); 1065 1066 add_task(async function test_telemetry_reports_if_parsing_fails() { 1067 await client.db.importChanges({}, 10000); 1068 1069 const startSnapshot = getUptakeTelemetrySnapshot( 1070 TELEMETRY_COMPONENT, 1071 client.identifier 1072 ); 1073 1074 try { 1075 await client.maybeSync(10001); 1076 } catch (e) {} 1077 1078 const endSnapshot = getUptakeTelemetrySnapshot( 1079 TELEMETRY_COMPONENT, 1080 client.identifier 1081 ); 1082 const expectedIncrements = { 1083 [UptakeTelemetry.STATUS.SYNC_START]: 1, 1084 [UptakeTelemetry.STATUS.PARSE_ERROR]: 1, 1085 }; 1086 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 1087 }); 1088 add_task(clear_state); 1089 1090 add_task(async function test_telemetry_reports_if_fetching_signature_fails() { 1091 await client.db.importChanges({}, 11000); 1092 1093 const startSnapshot = getUptakeTelemetrySnapshot( 1094 TELEMETRY_COMPONENT, 1095 client.identifier 1096 ); 1097 1098 try { 1099 await client.maybeSync(11001); 1100 } catch (e) {} 1101 1102 const endSnapshot = getUptakeTelemetrySnapshot( 1103 TELEMETRY_COMPONENT, 1104 client.identifier 1105 ); 1106 const expectedIncrements = { 1107 [UptakeTelemetry.STATUS.SYNC_START]: 1, 1108 [UptakeTelemetry.STATUS.SERVER_ERROR]: 1, 1109 }; 1110 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 1111 }); 1112 add_task(clear_state); 1113 1114 add_task(async function test_telemetry_reports_unknown_errors() { 1115 const backup = client.db.getLastModified; 1116 client.db.getLastModified = () => { 1117 throw new Error("Internal"); 1118 }; 1119 const startSnapshot = getUptakeTelemetrySnapshot( 1120 TELEMETRY_COMPONENT, 1121 client.identifier 1122 ); 1123 1124 try { 1125 await client.maybeSync(2000); 1126 } catch (e) {} 1127 1128 client.db.getLastModified = backup; 1129 const endSnapshot = getUptakeTelemetrySnapshot( 1130 TELEMETRY_COMPONENT, 1131 client.identifier 1132 ); 1133 const expectedIncrements = { 1134 [UptakeTelemetry.STATUS.SYNC_START]: 1, 1135 [UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1, 1136 }; 1137 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 1138 }); 1139 add_task(clear_state); 1140 1141 add_task(async function test_telemetry_reports_indexeddb_as_custom_1() { 1142 const backup = client.db.getLastModified; 1143 const msg = 1144 "IndexedDB getLastModified() The operation failed for reasons unrelated to the database itself"; 1145 client.db.getLastModified = () => { 1146 throw new Error(msg); 1147 }; 1148 const startSnapshot = getUptakeTelemetrySnapshot( 1149 TELEMETRY_COMPONENT, 1150 client.identifier 1151 ); 1152 1153 try { 1154 await client.maybeSync(2000); 1155 } catch (e) {} 1156 1157 client.db.getLastModified = backup; 1158 const endSnapshot = getUptakeTelemetrySnapshot( 1159 TELEMETRY_COMPONENT, 1160 client.identifier 1161 ); 1162 const expectedIncrements = { 1163 [UptakeTelemetry.STATUS.SYNC_START]: 1, 1164 [UptakeTelemetry.STATUS.CUSTOM_1_ERROR]: 1, 1165 }; 1166 checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements); 1167 }); 1168 add_task(clear_state); 1169 1170 add_task(async function test_telemetry_reports_error_name_as_event_nightly() { 1171 const backup = client.db.getLastModified; 1172 client.db.getLastModified = () => { 1173 const e = new Error("Some unknown error"); 1174 e.name = "ThrownError"; 1175 throw e; 1176 }; 1177 1178 try { 1179 await client.maybeSync(2000); 1180 } catch (e) {} 1181 1182 TelemetryTestUtils.assertEvents( 1183 [ 1184 [ 1185 "uptake.remotecontent.result", 1186 "uptake", 1187 "remotesettings", 1188 UptakeTelemetry.STATUS.SYNC_START, 1189 { 1190 source: client.identifier, 1191 trigger: "manual", 1192 }, 1193 ], 1194 [ 1195 "uptake.remotecontent.result", 1196 "uptake", 1197 "remotesettings", 1198 UptakeTelemetry.STATUS.UNKNOWN_ERROR, 1199 { 1200 source: client.identifier, 1201 trigger: "manual", 1202 duration: v => v >= 0, 1203 errorName: "ThrownError", 1204 }, 1205 ], 1206 ], 1207 TELEMETRY_EVENTS_FILTERS 1208 ); 1209 1210 client.db.getLastModified = backup; 1211 }); 1212 add_task(clear_state); 1213 1214 add_task(async function test_bucketname_changes_when_preview_mode_is_enabled() { 1215 equal(client.bucketName, "main"); 1216 1217 RemoteSettings.enablePreviewMode(true); 1218 1219 equal(client.bucketName, "main-preview"); 1220 }); 1221 add_task(clear_state); 1222 1223 add_task( 1224 async function test_preview_mode_pref_affects_bucket_names_before_instantiated() { 1225 Services.prefs.setBoolPref("services.settings.preview_enabled", true); 1226 1227 let clientWithDefaultBucket = RemoteSettings("other"); 1228 let clientWithBucket = RemoteSettings("coll", { bucketName: "buck" }); 1229 1230 equal(clientWithDefaultBucket.bucketName, "main-preview"); 1231 equal(clientWithBucket.bucketName, "buck-preview"); 1232 } 1233 ); 1234 add_task(clear_state); 1235 1236 add_task( 1237 async function test_preview_enabled_pref_ignored_when_mode_is_set_explicitly() { 1238 Services.prefs.setBoolPref("services.settings.preview_enabled", true); 1239 1240 let clientWithDefaultBucket = RemoteSettings("other"); 1241 let clientWithBucket = RemoteSettings("coll", { bucketName: "buck" }); 1242 1243 equal(clientWithDefaultBucket.bucketName, "main-preview"); 1244 equal(clientWithBucket.bucketName, "buck-preview"); 1245 1246 RemoteSettings.enablePreviewMode(false); 1247 1248 equal(clientWithDefaultBucket.bucketName, "main"); 1249 equal(clientWithBucket.bucketName, "buck"); 1250 } 1251 ); 1252 add_task(clear_state); 1253 1254 add_task( 1255 async function test_get_loads_default_records_from_a_local_dump_when_preview_mode_is_enabled() { 1256 if (IS_ANDROID) { 1257 // Skip test: we don't ship remote settings dumps on Android (see package-manifest). 1258 return; 1259 } 1260 RemoteSettings.enablePreviewMode(true); 1261 // When collection has a dump in services/settings/dumps/{bucket}/{collection}.json 1262 const data = await clientWithDump.get(); 1263 notEqual(data.length, 0); 1264 // No synchronization happened (responses are not mocked). 1265 } 1266 ); 1267 add_task(clear_state); 1268 1269 add_task(async function test_local_db_distinguishes_preview_records() { 1270 RemoteSettings.enablePreviewMode(true); 1271 client.db.importChanges({}, Date.now(), [{ id: "record-1" }], { 1272 clear: true, 1273 }); 1274 1275 RemoteSettings.enablePreviewMode(false); 1276 client.db.importChanges({}, Date.now(), [{ id: "record-2" }], { 1277 clear: true, 1278 }); 1279 1280 deepEqual(await client.get(), [{ id: "record-2" }]); 1281 }); 1282 add_task(clear_state); 1283 1284 add_task( 1285 async function test_inspect_changes_the_list_when_preview_mode_is_enabled() { 1286 if (IS_ANDROID) { 1287 // Skip test: we don't ship remote settings dumps on Android (see package-manifest), 1288 // and this test relies on the fact that clients are instantiated if a dump is packaged. 1289 return; 1290 } 1291 1292 // Register a client only listed in -preview... 1293 RemoteSettings("crash-rate"); 1294 1295 const { collections: before, previewMode: previewModeBefore } = 1296 await RemoteSettings.inspect(); 1297 1298 Assert.ok(!previewModeBefore, "preview is not enabled"); 1299 1300 // These two collections are listed in the main bucket in monitor/changes (one with dump, one registered). 1301 deepEqual(before.map(c => c.collection).sort(), [ 1302 "language-dictionaries", 1303 "password-fields", 1304 ]); 1305 1306 // Switch to preview mode. 1307 RemoteSettings.enablePreviewMode(true); 1308 1309 const { 1310 collections: after, 1311 mainBucket, 1312 previewMode, 1313 } = await RemoteSettings.inspect(); 1314 1315 Assert.ok(previewMode, "preview is enabled"); 1316 1317 // These two collections are listed in the main bucket in monitor/changes (both are registered). 1318 deepEqual(after.map(c => c.collection).sort(), [ 1319 "crash-rate", 1320 "password-fields", 1321 ]); 1322 equal(mainBucket, "main-preview"); 1323 } 1324 ); 1325 add_task(clear_state); 1326 1327 add_task(async function test_sync_event_is_not_sent_from_get_when_no_dump() { 1328 let called = false; 1329 client.on("sync", () => { 1330 called = true; 1331 }); 1332 1333 await client.get(); 1334 1335 Assert.ok(!called, "sync event is not sent from .get()"); 1336 }); 1337 add_task(clear_state); 1338 1339 add_task(async function test_get_can_be_called_from_sync_event_callback() { 1340 let fromGet; 1341 let fromEvent; 1342 1343 client.on("sync", async ({ data: { current } }) => { 1344 // Before fixing Bug 1761953 this would result in a deadlock. 1345 fromGet = await client.get(); 1346 fromEvent = current; 1347 }); 1348 1349 await client.maybeSync(2000); 1350 1351 Assert.ok(fromGet, "sync callback was called"); 1352 Assert.deepEqual(fromGet, fromEvent, ".get() gives current records list"); 1353 }); 1354 add_task(clear_state); 1355 1356 add_task(async function test_attachments_are_pruned_when_sync_from_timer() { 1357 await client.db.saveAttachment("bar", { 1358 record: { id: "bar" }, 1359 blob: new Blob(["456"]), 1360 }); 1361 1362 await client.maybeSync(2000, { trigger: "broadcast" }); 1363 1364 Assert.ok( 1365 await client.attachments.cacheImpl.get("bar"), 1366 "Extra attachment was not deleted on broadcast" 1367 ); 1368 1369 await client.maybeSync(3001, { trigger: "timer" }); 1370 1371 Assert.ok( 1372 !(await client.attachments.cacheImpl.get("bar")), 1373 "Extra attachment was deleted on timer" 1374 ); 1375 }); 1376 add_task(clear_state); 1377 1378 function handleResponse(request, response) { 1379 try { 1380 const sample = getSampleResponse(request, server.identity.primaryPort); 1381 if (!sample) { 1382 do_throw( 1383 `unexpected ${request.method} request for ${request.path}?${request.queryString}` 1384 ); 1385 } 1386 1387 response.setStatusLine( 1388 null, 1389 sample.status.status, 1390 sample.status.statusText 1391 ); 1392 // send the headers 1393 for (let headerLine of sample.sampleHeaders) { 1394 let headerElements = headerLine.split(":"); 1395 response.setHeader(headerElements[0], headerElements[1].trimLeft()); 1396 } 1397 response.setHeader("Date", new Date().toUTCString()); 1398 1399 const body = 1400 typeof sample.responseBody == "string" 1401 ? sample.responseBody 1402 : JSON.stringify(sample.responseBody); 1403 response.write(body); 1404 response.finish(); 1405 } catch (e) { 1406 info(e); 1407 } 1408 } 1409 1410 function getSampleResponse(req, port) { 1411 const responses = { 1412 OPTIONS: { 1413 sampleHeaders: [ 1414 "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page", 1415 "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS", 1416 "Access-Control-Allow-Origin: *", 1417 "Content-Type: application/json; charset=UTF-8", 1418 "Server: waitress", 1419 ], 1420 status: { status: 200, statusText: "OK" }, 1421 responseBody: null, 1422 }, 1423 "GET:/v1/": { 1424 sampleHeaders: [ 1425 "Access-Control-Allow-Origin: *", 1426 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1427 "Content-Type: application/json; charset=UTF-8", 1428 "Server: waitress", 1429 ], 1430 status: { status: 200, statusText: "OK" }, 1431 responseBody: { 1432 settings: { 1433 batch_max_requests: 25, 1434 }, 1435 url: `http://localhost:${port}/v1/`, 1436 documentation: "https://kinto.readthedocs.org/", 1437 version: "1.5.1", 1438 commit: "cbc6f58", 1439 hello: "kinto", 1440 }, 1441 }, 1442 "GET:/v1/buckets/monitor/collections/changes/changeset": { 1443 sampleHeaders: [ 1444 "Access-Control-Allow-Origin: *", 1445 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1446 "Content-Type: application/json; charset=UTF-8", 1447 "Server: waitress", 1448 `Date: ${new Date().toUTCString()}`, 1449 'Etag: "5000"', 1450 ], 1451 status: { status: 200, statusText: "OK" }, 1452 responseBody: { 1453 timestamp: 5000, 1454 changes: [ 1455 { 1456 id: "4676f0c7-9757-4796-a0e8-b40a5a37a9c9", 1457 bucket: "main", 1458 collection: "unknown-locally", 1459 last_modified: 5000, 1460 }, 1461 { 1462 id: "4676f0c7-9757-4796-a0e8-b40a5a37a9c9", 1463 bucket: "main", 1464 collection: "language-dictionaries", 1465 last_modified: 4000, 1466 }, 1467 { 1468 id: "0af8da0b-3e03-48fb-8d0d-2d8e4cb7514d", 1469 bucket: "main", 1470 collection: "password-fields", 1471 last_modified: 3000, 1472 }, 1473 { 1474 id: "4acda969-3bd3-4074-a678-ff311eeb076e", 1475 bucket: "main-preview", 1476 collection: "password-fields", 1477 last_modified: 2000, 1478 }, 1479 { 1480 id: "58697bd1-315f-4185-9bee-3371befc2585", 1481 bucket: "main-preview", 1482 collection: "crash-rate", 1483 last_modified: 1000, 1484 }, 1485 ], 1486 }, 1487 }, 1488 "GET:/fake-x5u": { 1489 sampleHeaders: ["Content-Type: application/octet-stream"], 1490 status: { status: 200, statusText: "OK" }, 1491 responseBody: `-----BEGIN CERTIFICATE----- 1492 MIIGYTCCBEmgAwIBAgIBATANBgkqhkiG9w0BAQwFADB9MQswCQYDVQQGEwJVU 1493 ZARKjbu1TuYQHf0fs+GwID8zeLc2zJL7UzcHFwwQ6Nda9OJN4uPAuC/BKaIpxCLL 1494 26b24/tRam4SJjqpiq20lynhUrmTtt6hbG3E1Hpy3bmkt2DYnuMFwEx2gfXNcnbT 1495 wNuvFqc= 1496 -----END CERTIFICATE-----`, 1497 }, 1498 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=2000": 1499 { 1500 sampleHeaders: [ 1501 "Access-Control-Allow-Origin: *", 1502 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1503 "Content-Type: application/json; charset=UTF-8", 1504 "Server: waitress", 1505 'Etag: "3000"', 1506 ], 1507 status: { status: 200, statusText: "OK" }, 1508 responseBody: { 1509 timestamp: 3000, 1510 metadata: { 1511 id: "password-fields", 1512 last_modified: 1234, 1513 signatures: [ 1514 { 1515 signature: "abcdef", 1516 x5u: `http://localhost:${port}/fake-x5u`, 1517 }, 1518 ], 1519 }, 1520 changes: [ 1521 { 1522 id: "9d500963-d80e-3a91-6e74-66f3811b99cc", 1523 last_modified: 3000, 1524 website: "https://some-website.com", 1525 selector: "#user[password]", 1526 }, 1527 ], 1528 }, 1529 }, 1530 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=%223000%22": 1531 { 1532 sampleHeaders: [ 1533 "Access-Control-Allow-Origin: *", 1534 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1535 "Content-Type: application/json; charset=UTF-8", 1536 "Server: waitress", 1537 'Etag: "4000"', 1538 ], 1539 status: { status: 200, statusText: "OK" }, 1540 responseBody: { 1541 metadata: { 1542 signatures: [{}], 1543 }, 1544 timestamp: 4000, 1545 changes: [ 1546 { 1547 id: "aabad965-e556-ffe7-4191-074f5dee3df3", 1548 last_modified: 4000, 1549 website: "https://www.other.org/signin", 1550 selector: "#signinpassword", 1551 }, 1552 { 1553 id: "9d500963-d80e-3a91-6e74-66f3811b99cc", 1554 last_modified: 3500, 1555 website: "https://some-website.com/login", 1556 selector: "input#user[password]", 1557 }, 1558 ], 1559 }, 1560 }, 1561 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=%224000%22": 1562 { 1563 sampleHeaders: [ 1564 "Access-Control-Allow-Origin: *", 1565 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1566 "Content-Type: application/json; charset=UTF-8", 1567 "Server: waitress", 1568 'Etag: "5000"', 1569 ], 1570 status: { status: 200, statusText: "OK" }, 1571 responseBody: { 1572 metadata: { 1573 signatures: [{}], 1574 }, 1575 timestamp: 5000, 1576 changes: [ 1577 { 1578 id: "aabad965-e556-ffe7-4191-074f5dee3df3", 1579 deleted: true, 1580 }, 1581 ], 1582 }, 1583 }, 1584 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=%229999%22": 1585 { 1586 sampleHeaders: [ 1587 "Access-Control-Allow-Origin: *", 1588 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1589 "Content-Type: application/json; charset=UTF-8", 1590 "Server: waitress", 1591 ], 1592 status: { status: 503, statusText: "Service Unavailable" }, 1593 responseBody: { 1594 code: 503, 1595 errno: 999, 1596 error: "Service Unavailable", 1597 }, 1598 }, 1599 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=%2210000%22": 1600 { 1601 sampleHeaders: [ 1602 "Access-Control-Allow-Origin: *", 1603 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1604 "Content-Type: application/json; charset=UTF-8", 1605 "Server: waitress", 1606 'Etag: "10001"', 1607 ], 1608 status: { status: 200, statusText: "OK" }, 1609 responseBody: "<invalid json", 1610 }, 1611 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=%2211000%22": 1612 { 1613 sampleHeaders: [ 1614 "Access-Control-Allow-Origin: *", 1615 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1616 "Content-Type: application/json; charset=UTF-8", 1617 "Server: waitress", 1618 ], 1619 status: { status: 503, statusText: "Service Unavailable" }, 1620 responseBody: { 1621 changes: [ 1622 { 1623 id: "c4f021e3-f68c-4269-ad2a-d4ba87762b35", 1624 last_modified: 4000, 1625 website: "https://www.eff.org", 1626 selector: "#pwd", 1627 }, 1628 ], 1629 }, 1630 }, 1631 "GET:/v1/buckets/main/collections/password-fields?_expected=11001": { 1632 sampleHeaders: [ 1633 "Access-Control-Allow-Origin: *", 1634 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1635 "Content-Type: application/json; charset=UTF-8", 1636 "Server: waitress", 1637 ], 1638 status: { status: 503, statusText: "Service Unavailable" }, 1639 responseBody: { 1640 code: 503, 1641 errno: 999, 1642 error: "Service Unavailable", 1643 }, 1644 }, 1645 "GET:/v1/buckets/monitor/collections/changes/changeset?collection=password-fields&bucket=main&_expected=0": 1646 { 1647 sampleHeaders: [ 1648 "Access-Control-Allow-Origin: *", 1649 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1650 "Content-Type: application/json; charset=UTF-8", 1651 "Server: waitress", 1652 `Date: ${new Date().toUTCString()}`, 1653 'Etag: "1338"', 1654 ], 1655 status: { status: 200, statusText: "OK" }, 1656 responseBody: { 1657 timestamp: 1338, 1658 changes: [ 1659 { 1660 id: "fe5758d0-c67a-42d0-bb4f-8f2d75106b65", 1661 bucket: "main", 1662 collection: "password-fields", 1663 last_modified: 1337, 1664 }, 1665 ], 1666 }, 1667 }, 1668 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337": 1669 { 1670 sampleHeaders: [ 1671 "Access-Control-Allow-Origin: *", 1672 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1673 "Content-Type: application/json; charset=UTF-8", 1674 "Server: waitress", 1675 'Etag: "3000"', 1676 ], 1677 status: { status: 200, statusText: "OK" }, 1678 responseBody: { 1679 metadata: { 1680 signatures: [ 1681 { 1682 signature: "some-sig", 1683 x5u: `http://localhost:${port}/fake-x5u`, 1684 }, 1685 ], 1686 }, 1687 timestamp: 3000, 1688 changes: [ 1689 { 1690 id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69", 1691 last_modified: 3000, 1692 website: "https://some-website.com", 1693 selector: "#webpage[field-pwd]", 1694 }, 1695 ], 1696 }, 1697 }, 1698 "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=%223000%22": 1699 { 1700 sampleHeaders: [ 1701 "Access-Control-Allow-Origin: *", 1702 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1703 "Content-Type: application/json; charset=UTF-8", 1704 "Server: waitress", 1705 'Etag: "3001"', 1706 ], 1707 status: { status: 200, statusText: "OK" }, 1708 responseBody: { 1709 metadata: { 1710 signatures: [ 1711 { 1712 signature: "some-sig", 1713 x5u: `http://localhost:${port}/fake-x5u`, 1714 }, 1715 ], 1716 }, 1717 timestamp: 3001, 1718 changes: [ 1719 { 1720 id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69", 1721 last_modified: 3001, 1722 website: "https://some-website-2.com", 1723 selector: "#webpage[field-pwd]", 1724 }, 1725 ], 1726 }, 1727 }, 1728 "GET:/v1/buckets/main/collections/language-dictionaries/changeset": { 1729 sampleHeaders: [ 1730 "Access-Control-Allow-Origin: *", 1731 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1732 "Content-Type: application/json; charset=UTF-8", 1733 "Server: waitress", 1734 'Etag: "5000000000000"', 1735 ], 1736 status: { status: 200, statusText: "OK" }, 1737 responseBody: { 1738 timestamp: 5000000000000, 1739 metadata: { 1740 id: "language-dictionaries", 1741 last_modified: 1234, 1742 signatures: [ 1743 { 1744 signature: "xyz", 1745 x5u: `http://localhost:${port}/fake-x5u`, 1746 }, 1747 ], 1748 }, 1749 changes: [ 1750 { 1751 id: "xx", 1752 last_modified: 5000000000000, 1753 dictionaries: ["xx-XX@dictionaries.addons.mozilla.org"], 1754 }, 1755 { 1756 id: "fr", 1757 last_modified: 5000000000000 - 1, 1758 deleted: true, 1759 }, 1760 { 1761 id: "pt-BR", 1762 last_modified: 5000000000000 - 2, 1763 dictionaries: ["pt-BR@for-tests"], 1764 }, 1765 ], 1766 }, 1767 }, 1768 "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=2000": 1769 { 1770 sampleHeaders: [ 1771 "Access-Control-Allow-Origin: *", 1772 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1773 "Content-Type: application/json; charset=UTF-8", 1774 "Server: waitress", 1775 'Etag: "2000"', 1776 ], 1777 status: { status: 200, statusText: "OK" }, 1778 responseBody: { 1779 timestamp: 2000, 1780 metadata: { 1781 id: "with-local-fields", 1782 last_modified: 1234, 1783 signatures: [ 1784 { 1785 signature: "xyz", 1786 x5u: `http://localhost:${port}/fake-x5u`, 1787 }, 1788 ], 1789 }, 1790 changes: [ 1791 { 1792 id: "c74279ce-fb0a-42a6-ae11-386b567a6119", 1793 last_modified: 2000, 1794 }, 1795 ], 1796 }, 1797 }, 1798 "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=%222000%22": 1799 { 1800 sampleHeaders: [ 1801 "Access-Control-Allow-Origin: *", 1802 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1803 "Content-Type: application/json; charset=UTF-8", 1804 "Server: waitress", 1805 'Etag: "3000"', 1806 ], 1807 status: { status: 200, statusText: "OK" }, 1808 responseBody: { 1809 timestamp: 3000, 1810 metadata: { 1811 signatures: [{}], 1812 }, 1813 changes: [ 1814 { 1815 id: "1f5c98b9-6d93-4c13-aa26-978b38695096", 1816 last_modified: 3000, 1817 }, 1818 ], 1819 }, 1820 }, 1821 "GET:/v1/buckets/monitor/collections/changes/changeset?collection=no-mocked-responses&bucket=main&_expected=0": 1822 { 1823 sampleHeaders: [ 1824 "Access-Control-Allow-Origin: *", 1825 "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", 1826 "Content-Type: application/json; charset=UTF-8", 1827 "Server: waitress", 1828 `Date: ${new Date().toUTCString()}`, 1829 'Etag: "713705"', 1830 ], 1831 status: { status: 200, statusText: "OK" }, 1832 responseBody: { 1833 data: [ 1834 { 1835 id: "07a98d1b-7c62-4344-ab18-76856b3facd8", 1836 bucket: "main", 1837 collection: "no-mocked-responses", 1838 last_modified: 713705, 1839 }, 1840 ], 1841 }, 1842 }, 1843 }; 1844 return ( 1845 responses[`${req.method}:${req.path}?${req.queryString}`] || 1846 responses[`${req.method}:${req.path}`] || 1847 responses[req.method] 1848 ); 1849 } 1850 1851 add_task(clear_state); 1852 1853 add_task(async function test_hasAttachments_works_as_expected() { 1854 let res = await client.db.hasAttachments(); 1855 Assert.equal(res, false, "Should return false, no attachments at start"); 1856 1857 await client.db.saveAttachment("foo", { 1858 record: { id: "foo" }, 1859 blob: new Blob(["foo"]), 1860 }); 1861 1862 res = await client.db.hasAttachments(); 1863 Assert.equal(res, true, "Should return true, just saved an attachment"); 1864 1865 await client.db.pruneAttachments([]); 1866 1867 res = await client.db.hasAttachments(); 1868 Assert.equal(res, false, "Should return false after attachments are pruned"); 1869 }); 1870 add_task(clear_state);