add_task(() => { // Set up an HTTP Server
server = new HttpServer();
server.start(-1);
// Pretend we are in nightly channel to make sure all telemetry events are sent.
let oldGetChannel = Policy.getChannel;
Policy.getChannel = () => "nightly";
// Point the blocklist clients to use this local HTTP server.
Services.prefs.setStringPref( "services.settings.server",
`http://localhost:${server.identity.primaryPort}/v1`
);
add_task(async function test_records_obtained_from_server_are_stored_in_db() { // Test an empty db populates
await client.maybeSync(2000);
// Open the collection, verify it's been populated: // Our test data has a single record; it should be in the local collection const list = await client.get();
equal(list.length, 1);
const timestamp = await client.db.getLastModified();
equal(timestamp, 3000, "timestamp was stored");
add_task(
async function test_records_from_dump_are_listed_as_created_in_event() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
}
let received;
clientWithDump.on("sync", ({ data }) => (received = data)); // Use a timestamp superior to latest record in dump. const timestamp = 5000000000000; // Fri Jun 11 2128
await clientWithDump.maybeSync(timestamp);
const list = await clientWithDump.get(); Assert.greater(
list.length,
20,
`The dump was loaded (${list.length} records)`
);
equal(received.created[0].id, "xx", "Record from the sync come first.");
ok(
!(received.deleted[0].id in createdById), "Deleted records are not listed as created"
);
equal(
createdById[received.updated[0].new.id],
received.updated[0].new, "The records that were updated should appear as created in their newest form."
);
equal(
received.created.length,
list.length, "The list of created records contains the dump"
);
equal(received.current.length, received.created.length);
}
);
add_task(clear_state);
add_task(async function test_sync_event_is_sent_even_if_up_to_date() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
} // First, determine what is the dump timestamp. Sync will load it. // Use a timestamp inferior to latest record in dump.
await clientWithDump._importJSONDump(); const uptodateTimestamp = await clientWithDump.db.getLastModified();
await clear_state();
// Now, simulate that server data wasn't changed since dump was released. const startSnapshot = getUptakeTelemetrySnapshot(
TELEMETRY_COMPONENT,
clientWithDump.identifier
);
let received;
clientWithDump.on("sync", ({ data }) => (received = data));
add_task(async function test_records_can_have_local_fields() { const c = RemoteSettings("with-local-fields", { localFields: ["accepted"] });
c.verifySignature = false;
await c.maybeSync(2000);
await c.db.update({
id: "c74279ce-fb0a-42a6-ae11-386b567a6119",
accepted: true,
});
await c.maybeSync(3000); // Does not fail.
});
add_task(clear_state);
add_task(
async function test_records_changes_are_overwritten_by_server_changes() { // Create some local conflicting data, and make sure it syncs without error.
await client.db.create({
website: "",
id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
});
await client.maybeSync(2000);
const data = await client.get();
equal(data[0].website, "https://some-website.com");
}
);
add_task(clear_state);
add_task(
async function test_get_loads_default_records_from_a_local_dump_when_database_is_empty() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
}
// When collection has a dump in services/settings/dumps/{bucket}/{collection}.json const data = await clientWithDump.get();
notEqual(data.length, 0); // No synchronization happened (responses are not mocked).
}
);
add_task(clear_state);
add_task(async function test_get_loads_dump_only_once_if_called_in_parallel() { const backup = clientWithDump._importJSONDump;
let callCount = 0;
clientWithDump._importJSONDump = async () => {
callCount++; // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 100)); return 42;
};
await Promise.all([clientWithDump.get(), clientWithDump.get()]);
equal(callCount, 1, "JSON dump was called more than once");
clientWithDump._importJSONDump = backup;
});
add_task(clear_state);
add_task(async function test_get_falls_back_to_dump_if_db_fails() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
} const backup = clientWithDump.db.getLastModified;
clientWithDump.db.getLastModified = () => { thrownew Error("Unknown error");
};
const records = await clientWithDump.get({ dumpFallback: true });
ok(!!records.length, "dump content is returned");
// If fallback is disabled, error is thrown.
let error; try {
await clientWithDump.get({ dumpFallback: false });
} catch (e) {
error = e;
}
equal(error.message, "Unknown error");
add_task(async function test_get_does_not_sync_if_empty_dump_is_provided() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
}
add_task(async function test_get_synchronization_can_be_disabled() { const data = await client.get({ syncIfEmpty: false });
equal(data.length, 0);
});
add_task(clear_state);
add_task(
async function test_get_triggers_synchronization_when_database_is_empty() { // The "password-fields" collection has no local dump, and no local data. // Therefore a synchronization will happen. const data = await client.get();
// Data comes from mocked HTTP response (see below).
equal(data.length, 1);
equal(data[0].selector, "#webpage[field-pwd]");
}
);
add_task(clear_state);
add_task(async function test_get_ignores_synchronization_errors_by_default() { // The monitor endpoint won't contain any information about this collection.
let data = await RemoteSettings("some-unknown-key").get();
equal(data.length, 0); // The sync endpoints are not mocked, this fails internally.
data = await RemoteSettings("no-mocked-responses").get();
equal(data.length, 0);
});
add_task(clear_state);
add_task(async function test_get_throws_if_no_empty_fallback() { // The monitor endpoint won't contain any information about this collection. try {
await RemoteSettings("some-unknown-key").get({
emptyListFallback: false,
}); Assert.ok(false, ".get() should throw");
} catch (error) { Assert.ok(
error.message.includes("Response from server unparseable"), "Server error was thrown"
);
}
});
add_task(clear_state);
add_task(async function test_get_verify_signature_no_sync() { // No signature in metadata, and no sync if empty.
let error; try {
await client.get({ verifySignature: true, syncIfEmpty: false });
} catch (e) {
error = e;
}
equal(error.message, "Missing signature (main/password-fields)");
});
add_task(clear_state);
add_task(async function test_get_can_verify_signature_pulled() { // Populate the local DB (only records, eg. loaded from dump previously)
await client._importJSONDump();
// No metadata in local DB, but gets pulled and then verifies.
ok(ObjectUtils.isEmpty(await client.db.getMetadata()), "Metadata is empty");
await client.get({ verifySignature: true });
ok(
!ObjectUtils.isEmpty(await client.db.getMetadata()), "Metadata was pulled"
);
ok(calledSignature.endsWith("some-sig"), "Signature was verified");
});
add_task(clear_state);
add_task(async function test_get_can_verify_signature() { // Populate the local DB (record and metadata)
await client.maybeSync(2000);
// It validates signature that was stored in local DB.
let calledSignature;
client._verifier = {
async asyncVerifyContentSignature(serialized, signature) {
calledSignature = signature; return JSON.parse(serialized).data.length == 1;
},
};
ok(await Utils.hasLocalData(client), "Local data was populated");
await client.get({ verifySignature: true });
ok(calledSignature.endsWith("abcdef"), "Signature was verified");
// It throws when signature does not verify.
await client.db.delete("9d500963-d80e-3a91-6e74-66f3811b99cc");
let error = null; try {
await client.get({ verifySignature: true });
} catch (e) {
error = e;
}
equal(
error.message, "Invalid content signature (main/password-fields) using 'fake-x5u' and signer remote-settings.content-signature.mozilla.org"
);
});
add_task(clear_state);
add_task(async function test_get_does_not_verify_signature_if_load_dump() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
}
let called;
clientWithDump._verifier = {
async asyncVerifyContentSignature() {
called = true; returntrue;
},
};
// When dump is loaded, signature is not verified. const records = await clientWithDump.get({ verifySignature: true });
ok(!!records.length, "dump is loaded");
ok(!called, "signature is missing but not verified");
// If metadata is missing locally, it is not fetched if `syncIfEmpty` is disabled.
let error; try {
await clientWithDump.get({ verifySignature: true, syncIfEmpty: false });
} catch (e) {
error = e;
}
ok(!called, "signer was not called");
equal(
error.message, "Missing signature (main/language-dictionaries)", "signature is missing locally"
);
// If metadata is missing locally, it is fetched by default (`syncIfEmpty: true`)
await clientWithDump.get({ verifySignature: true }); const metadata = await clientWithDump.db.getMetadata();
ok(!!Object.keys(metadata).length, "metadata was fetched");
ok(called, "signature was verified for the data that was in dump");
});
add_task(clear_state);
add_task(
async function test_get_does_verify_signature_if_json_loaded_in_parallel() { const backup = clientWithDump._verifier;
let callCount = 0;
clientWithDump._verifier = {
async asyncVerifyContentSignature() {
callCount++; returntrue;
},
};
await Promise.all([
clientWithDump.get({ verifySignature: true }),
clientWithDump.get({ verifySignature: true }),
]);
equal(callCount, 0, "No need to verify signatures if JSON dump is loaded");
clientWithDump._verifier = backup;
}
);
add_task(clear_state);
ok(
messages.includes("main/password-fields sync already running"), "warning is shown about sync already running"
);
Utils.log.warn = backup;
});
add_task(clear_state);
add_task(
async function test_sync_pulls_metadata_if_missing_with_dump_is_up_to_date() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
}
let called;
clientWithDump._verifier = {
async asyncVerifyContentSignature() {
called = true; returntrue;
},
}; // When dump is loaded, signature is not verified. const records = await clientWithDump.get({ verifySignature: true });
ok(!!records.length, "dump is loaded");
ok(!called, "signature is missing but not verified");
// Synchronize the collection (local data is up-to-date). // Signature verification is disabled (see `clear_state()`), so we don't bother with // fetching metadata. const uptodateTimestamp = await clientWithDump.db.getLastModified();
await clientWithDump.maybeSync(uptodateTimestamp);
let metadata = await clientWithDump.db.getMetadata();
ok(!metadata, "metadata was not fetched");
// Synchronize again the collection (up-to-date, since collection last modified still > 42)
clientWithDump.verifySignature = true;
await clientWithDump.maybeSync(42);
// With signature verification, metadata was fetched.
metadata = await clientWithDump.db.getMetadata();
ok(!!Object.keys(metadata).length, "metadata was fetched");
ok(called, "signature was verified for the data that was in dump");
// Metadata is present, signature will now verified.
called = false;
await clientWithDump.get({ verifySignature: true });
ok(called, "local signature is verified");
}
);
add_task(clear_state);
add_task(async function test_sync_event_provides_information_about_records() {
let eventData;
client.on("sync", ({ data }) => (eventData = data));
add_task(async function test_inspect_method() { // Synchronize the `password-fields` collection in order to have // some local data when .inspect() is called.
await client.maybeSync(2000);
// A collection is listed in .inspect() if it has local data or if there // is a JSON dump for it. // "password-fields" has no dump but was synchronized above and thus has local data.
let col = collections.pop();
equal(col.collection, "password-fields");
equal(col.serverTimestamp, 3000);
equal(col.localTimestamp, 3000);
if (!IS_ANDROID) { // "language-dictionaries" has a local dump (not on Android)
col = collections.pop();
equal(col.collection, "language-dictionaries");
equal(col.serverTimestamp, 4000);
ok(!col.localTimestamp); // not synchronized.
}
});
add_task(clear_state);
add_task(async function test_clearAll_method() { // Make sure we have some local data.
await client.maybeSync(2000);
await clientWithDump.maybeSync(2000);
await RemoteSettings.clearAll();
ok(!(await Utils.hasLocalData(client)), "Local data was deleted");
ok(!(await Utils.hasLocalData(clientWithDump)), "Local data was deleted");
ok(
!Services.prefs.prefHasUserValue(client.lastCheckTimePref), "Pref was cleaned"
);
// Synchronization is not broken after resuming.
await client.maybeSync(2000);
await clientWithDump.maybeSync(2000);
ok(await Utils.hasLocalData(client), "Local data was populated");
ok(await Utils.hasLocalData(clientWithDump), "Local data was populated");
});
add_task(clear_state);
add_task(async function test_listeners_are_not_deduplicated() {
let count = 0; const plus1 = () => {
count += 1;
};
add_task(async function test_telemetry_if_sync_succeeds() { // We test each client because Telemetry requires preleminary declarations. const startSnapshot = getUptakeTelemetrySnapshot(
TELEMETRY_COMPONENT,
client.identifier
);
add_task(
async function test_get_loads_default_records_from_a_local_dump_when_preview_mode_is_enabled() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest). return;
}
RemoteSettings.enablePreviewMode(true); // When collection has a dump in services/settings/dumps/{bucket}/{collection}.json const data = await clientWithDump.get();
notEqual(data.length, 0); // No synchronization happened (responses are not mocked).
}
);
add_task(clear_state);
add_task(
async function test_inspect_changes_the_list_when_preview_mode_is_enabled() { if (IS_ANDROID) { // Skip test: we don't ship remote settings dumps on Android (see package-manifest), // and this test relies on the fact that clients are instantiated if a dump is packaged. return;
}
// Register a client only listed in -preview...
RemoteSettings("crash-rate");
Assert.ok(!previewModeBefore, "preview is not enabled");
// These two collections are listed in the main bucket in monitor/changes (one with dump, one registered).
deepEqual(before.map(c => c.collection).sort(), [ "language-dictionaries", "password-fields",
]);
// Switch to preview mode.
RemoteSettings.enablePreviewMode(true);
// These two collections are listed in the main bucket in monitor/changes (both are registered).
deepEqual(after.map(c => c.collection).sort(), [ "crash-rate", "password-fields",
]);
equal(mainBucket, "main-preview");
}
);
add_task(clear_state);
add_task(async function test_sync_event_is_not_sent_from_get_when_no_dump() {
let called = false;
client.on("sync", () => {
called = true;
});
await client.get();
Assert.ok(!called, "sync event is not sent from .get()");
});
add_task(clear_state);
add_task(async function test_get_can_be_called_from_sync_event_callback() {
let fromGet;
let fromEvent;
client.on("sync", async ({ data: { current } }) => { // Before fixing Bug 1761953 this would result in a deadlock.
fromGet = await client.get();
fromEvent = current;
});
await client.maybeSync(2000);
Assert.ok(fromGet, "sync callback was called"); Assert.deepEqual(fromGet, fromEvent, ".get() gives current records list");
});
add_task(clear_state);
add_task(async function test_attachments_are_pruned_when_sync_from_timer() {
await client.db.saveAttachment("bar", {
record: { id: "bar" },
blob: new Blob(["456"]),
});
Assert.ok(
!(await client.attachments.cacheImpl.get("bar")), "Extra attachment was deleted on timer"
);
});
add_task(clear_state);
function handleResponse(request, response) { try { const sample = getSampleResponse(request, server.identity.primaryPort); if (!sample) {
do_throw(
`unexpected ${request.method} request for ${request.path}?${request.queryString}`
);
}
response.setStatusLine( null,
sample.status.status,
sample.status.statusText
); // send the headers for (let headerLine of sample.sampleHeaders) {
let headerElements = headerLine.split(":");
response.setHeader(headerElements[0], headerElements[1].trimLeft());
}
response.setHeader("Date", new Date().toUTCString());
add_task(async function test_hasAttachments_works_as_expected() {
let res = await client.db.hasAttachments(); Assert.equal(res, false, "Should return false, no attachments at start");
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.