mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
465fe08726
This is to enhance our initial remote-settings sync processes for collections. We are able to bundle attachments for collections (targeting collections with many small attachments) to improve client performance. Ground work that will allow remote-settings consumers to download an attachment bundle. This patch itself does not change any existing client behaviors. Differential Revision: https://phabricator.services.mozilla.com/D216814
1687 lines
50 KiB
JavaScript
1687 lines
50 KiB
JavaScript
/* import-globals-from ../../../common/tests/unit/head_helpers.js */
|
|
|
|
const { ObjectUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/ObjectUtils.sys.mjs"
|
|
);
|
|
|
|
const IS_ANDROID = AppConstants.platform == "android";
|
|
|
|
const TELEMETRY_COMPONENT = "remotesettings";
|
|
const TELEMETRY_EVENTS_FILTERS = {
|
|
category: "uptake.remotecontent.result",
|
|
method: "uptake",
|
|
};
|
|
|
|
let server;
|
|
let client;
|
|
let clientWithDump;
|
|
|
|
async function clear_state() {
|
|
// Reset preview mode.
|
|
RemoteSettings.enablePreviewMode(undefined);
|
|
Services.prefs.clearUserPref("services.settings.preview_enabled");
|
|
|
|
client.verifySignature = false;
|
|
clientWithDump.verifySignature = false;
|
|
|
|
// Clear local DB.
|
|
await client.db.clear();
|
|
// Reset event listeners.
|
|
client._listeners.set("sync", []);
|
|
|
|
await clientWithDump.db.clear();
|
|
|
|
// Clear events snapshot.
|
|
TelemetryTestUtils.assertEvents([], {}, { process: "dummy" });
|
|
}
|
|
|
|
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`
|
|
);
|
|
|
|
Services.prefs.setStringPref("services.settings.loglevel", "debug");
|
|
|
|
client = RemoteSettings("password-fields");
|
|
clientWithDump = RemoteSettings("language-dictionaries");
|
|
|
|
server.registerPathHandler("/v1/", handleResponse);
|
|
server.registerPathHandler(
|
|
"/v1/buckets/monitor/collections/changes/changeset",
|
|
handleResponse
|
|
);
|
|
server.registerPathHandler(
|
|
"/v1/buckets/main/collections/password-fields/changeset",
|
|
handleResponse
|
|
);
|
|
server.registerPathHandler(
|
|
"/v1/buckets/main/collections/language-dictionaries/changeset",
|
|
handleResponse
|
|
);
|
|
server.registerPathHandler(
|
|
"/v1/buckets/main/collections/with-local-fields/changeset",
|
|
handleResponse
|
|
);
|
|
server.registerPathHandler("/fake-x5u", handleResponse);
|
|
|
|
registerCleanupFunction(() => {
|
|
Policy.getChannel = oldGetChannel;
|
|
server.stop(() => {});
|
|
});
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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");
|
|
|
|
const { signature } = await client.db.getMetadata();
|
|
equal(signature.signature, "abcdef", "metadata was stored");
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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.");
|
|
|
|
const createdById = received.created.reduce((acc, r) => {
|
|
acc[r.id] = r;
|
|
return acc;
|
|
}, {});
|
|
|
|
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_throws_when_network_is_offline() {
|
|
const backupOffline = Services.io.offline;
|
|
try {
|
|
Services.io.offline = true;
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
clientWithDump.identifier
|
|
);
|
|
let error;
|
|
try {
|
|
await clientWithDump.maybeSync(2000);
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
equal(error.name, "NetworkOfflineError");
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
clientWithDump.identifier
|
|
);
|
|
const expectedIncrements = {
|
|
[UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR]: 1,
|
|
};
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
} finally {
|
|
Services.io.offline = backupOffline;
|
|
}
|
|
});
|
|
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));
|
|
|
|
await clientWithDump.maybeSync(uptodateTimestamp);
|
|
|
|
ok(!!received.current.length, "Dump records are listed as created");
|
|
equal(received.current.length, received.created.length);
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
clientWithDump.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.UP_TO_DATE]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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 = () => {
|
|
throw new 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");
|
|
|
|
clientWithDump.db.getLastModified = backup;
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_get_sorts_results_if_specified() {
|
|
await client.db.importChanges(
|
|
{},
|
|
42,
|
|
[
|
|
{
|
|
field: 12,
|
|
id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
|
|
},
|
|
{
|
|
field: 7,
|
|
id: "d83444a4-f348-4cd8-8228-842cb927db9f",
|
|
},
|
|
],
|
|
{ clear: true }
|
|
);
|
|
|
|
const records = await client.get({ order: "field" });
|
|
Assert.less(
|
|
records[0].field,
|
|
records[records.length - 1].field,
|
|
"records are sorted"
|
|
);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_get_falls_back_sorts_results() {
|
|
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 = () => {
|
|
throw new Error("Unknown error");
|
|
};
|
|
|
|
const records = await clientWithDump.get({
|
|
dumpFallback: true,
|
|
order: "-id",
|
|
});
|
|
|
|
// eslint-disable-next-line mozilla/no-comparison-or-assignment-inside-ok
|
|
ok(records[0].id > records[records.length - 1].id, "records are sorted");
|
|
|
|
clientWithDump.db.getLastModified = backup;
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_get_falls_back_to_dump_if_db_fails_later() {
|
|
if (IS_ANDROID) {
|
|
// Skip test: we don't ship remote settings dumps on Android (see package-manifest).
|
|
return;
|
|
}
|
|
const backup = clientWithDump.db.list;
|
|
clientWithDump.db.list = () => {
|
|
throw new 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");
|
|
|
|
clientWithDump.db.list = backup;
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_get_falls_back_to_dump_if_network_fails() {
|
|
if (IS_ANDROID) {
|
|
// Skip test: we don't ship remote settings dumps on Android (see package-manifest).
|
|
return;
|
|
}
|
|
const backup = clientWithDump.sync;
|
|
clientWithDump.sync = () => {
|
|
throw new Error("Sync error");
|
|
};
|
|
|
|
const records = await clientWithDump.get();
|
|
ok(!!records.length, "dump content is returned");
|
|
|
|
clientWithDump.sync = backup;
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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;
|
|
}
|
|
|
|
const clientWithEmptyDump = RemoteSettings("example");
|
|
Assert.ok(!(await Utils.hasLocalData(clientWithEmptyDump)));
|
|
|
|
const data = await clientWithEmptyDump.get();
|
|
|
|
equal(data.length, 0);
|
|
Assert.ok(await Utils.hasLocalData(clientWithEmptyDump));
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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();
|
|
|
|
let calledSignature;
|
|
client._verifier = {
|
|
async asyncVerifyContentSignature(serialized, signature) {
|
|
calledSignature = signature;
|
|
return true;
|
|
},
|
|
};
|
|
client.verifySignature = true;
|
|
|
|
// 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'"
|
|
);
|
|
});
|
|
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;
|
|
return true;
|
|
},
|
|
};
|
|
|
|
// 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++;
|
|
return true;
|
|
},
|
|
};
|
|
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);
|
|
|
|
add_task(async function test_get_can_force_a_sync() {
|
|
const step0 = await client.db.getLastModified();
|
|
await client.get({ forceSync: true });
|
|
const step1 = await client.db.getLastModified();
|
|
await client.get();
|
|
const step2 = await client.db.getLastModified();
|
|
await client.get({ forceSync: true });
|
|
const step3 = await client.db.getLastModified();
|
|
|
|
equal(step0, null);
|
|
equal(step1, 3000);
|
|
equal(step2, 3000);
|
|
equal(step3, 3001);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_sync_runs_once_only() {
|
|
const backup = Utils.log.warn;
|
|
const messages = [];
|
|
Utils.log.warn = m => {
|
|
messages.push(m);
|
|
};
|
|
|
|
await Promise.all([client.maybeSync(2000), client.maybeSync(2000)]);
|
|
|
|
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;
|
|
return true;
|
|
},
|
|
};
|
|
// 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));
|
|
|
|
await client.maybeSync(2000);
|
|
equal(eventData.current.length, 1);
|
|
|
|
await client.maybeSync(3001);
|
|
equal(eventData.current.length, 2);
|
|
equal(eventData.created.length, 1);
|
|
equal(eventData.created[0].website, "https://www.other.org/signin");
|
|
equal(eventData.updated.length, 1);
|
|
equal(eventData.updated[0].old.website, "https://some-website.com");
|
|
equal(eventData.updated[0].new.website, "https://some-website.com/login");
|
|
equal(eventData.deleted.length, 0);
|
|
|
|
await client.maybeSync(4001);
|
|
equal(eventData.current.length, 1);
|
|
equal(eventData.created.length, 0);
|
|
equal(eventData.updated.length, 0);
|
|
equal(eventData.deleted.length, 1);
|
|
equal(eventData.deleted[0].website, "https://www.other.org/signin");
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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);
|
|
|
|
const inspected = await RemoteSettings.inspect();
|
|
|
|
// Assertion for global attributes.
|
|
const { mainBucket, serverURL, defaultSigner, collections, serverTimestamp } =
|
|
inspected;
|
|
const rsSigner = "remote-settings.content-signature.mozilla.org";
|
|
equal(mainBucket, "main");
|
|
equal(serverURL, `http://localhost:${server.identity.primaryPort}/v1`);
|
|
equal(defaultSigner, rsSigner);
|
|
equal(serverTimestamp, '"5000"');
|
|
|
|
// 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_inspect_method_uses_a_random_cache_bust() {
|
|
const backup = Utils.fetchLatestChanges;
|
|
const cacheBusts = [];
|
|
Utils.fetchLatestChanges = (url, options) => {
|
|
cacheBusts.push(options.expected);
|
|
return { changes: [] };
|
|
};
|
|
|
|
await RemoteSettings.inspect();
|
|
await RemoteSettings.inspect();
|
|
await RemoteSettings.inspect();
|
|
|
|
notEqual(cacheBusts[0], cacheBusts[1]);
|
|
notEqual(cacheBusts[1], cacheBusts[2]);
|
|
notEqual(cacheBusts[0], cacheBusts[2]);
|
|
Utils.fetchLatestChanges = backup;
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
client.on("sync", plus1);
|
|
client.on("sync", plus1);
|
|
client.on("sync", plus1);
|
|
|
|
await client.maybeSync(2000);
|
|
|
|
equal(count, 3);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_listeners_can_be_removed() {
|
|
let count = 0;
|
|
const onSync = () => {
|
|
count += 1;
|
|
};
|
|
|
|
client.on("sync", onSync);
|
|
client.off("sync", onSync);
|
|
|
|
await client.maybeSync(2000);
|
|
|
|
equal(count, 0);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_all_listeners_are_executed_if_one_fails() {
|
|
let count = 0;
|
|
client.on("sync", () => {
|
|
count += 1;
|
|
});
|
|
client.on("sync", () => {
|
|
throw new Error("boom");
|
|
});
|
|
client.on("sync", () => {
|
|
count += 2;
|
|
});
|
|
|
|
let error;
|
|
try {
|
|
await client.maybeSync(2000);
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
equal(count, 3);
|
|
equal(error.message, "boom");
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_up_to_date() {
|
|
await client.maybeSync(2000);
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
|
|
await client.maybeSync(3000);
|
|
|
|
// No Telemetry was sent.
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.UP_TO_DATE]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
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
|
|
);
|
|
|
|
await client.maybeSync(2000);
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.SUCCESS]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(
|
|
async function test_synchronization_duration_is_reported_in_uptake_status() {
|
|
await client.maybeSync(2000);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
[
|
|
"uptake.remotecontent.result",
|
|
"uptake",
|
|
"remotesettings",
|
|
UptakeTelemetry.STATUS.SUCCESS,
|
|
{
|
|
source: client.identifier,
|
|
duration: v => v > 0,
|
|
trigger: "manual",
|
|
},
|
|
],
|
|
],
|
|
TELEMETRY_EVENTS_FILTERS
|
|
);
|
|
}
|
|
);
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_if_application_fails() {
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
client.on("sync", () => {
|
|
throw new Error("boom");
|
|
});
|
|
|
|
try {
|
|
await client.maybeSync(2000);
|
|
} catch (e) {}
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.APPLY_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_if_sync_fails() {
|
|
await client.db.importChanges({}, 9999);
|
|
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
|
|
try {
|
|
await client.maybeSync(10000);
|
|
} catch (e) {}
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.SERVER_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_if_parsing_fails() {
|
|
await client.db.importChanges({}, 10000);
|
|
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
|
|
try {
|
|
await client.maybeSync(10001);
|
|
} catch (e) {}
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.PARSE_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_if_fetching_signature_fails() {
|
|
await client.db.importChanges({}, 11000);
|
|
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
|
|
try {
|
|
await client.maybeSync(11001);
|
|
} catch (e) {}
|
|
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.SERVER_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_unknown_errors() {
|
|
const backup = client.db.list;
|
|
client.db.list = () => {
|
|
throw new Error("Internal");
|
|
};
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
|
|
try {
|
|
await client.maybeSync(2000);
|
|
} catch (e) {}
|
|
|
|
client.db.list = backup;
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_indexeddb_as_custom_1() {
|
|
const backup = client.db.getLastModified;
|
|
const msg =
|
|
"IndexedDB getLastModified() The operation failed for reasons unrelated to the database itself";
|
|
client.db.getLastModified = () => {
|
|
throw new Error(msg);
|
|
};
|
|
const startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
|
|
try {
|
|
await client.maybeSync(2000);
|
|
} catch (e) {}
|
|
|
|
client.db.getLastModified = backup;
|
|
const endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
client.identifier
|
|
);
|
|
const expectedIncrements = { [UptakeTelemetry.STATUS.CUSTOM_1_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_telemetry_reports_error_name_as_event_nightly() {
|
|
const backup = client.db.list;
|
|
client.db.list = () => {
|
|
const e = new Error("Some unknown error");
|
|
e.name = "ThrownError";
|
|
throw e;
|
|
};
|
|
|
|
try {
|
|
await client.maybeSync(2000);
|
|
} catch (e) {}
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
[
|
|
"uptake.remotecontent.result",
|
|
"uptake",
|
|
"remotesettings",
|
|
UptakeTelemetry.STATUS.UNKNOWN_ERROR,
|
|
{
|
|
source: client.identifier,
|
|
trigger: "manual",
|
|
duration: v => v >= 0,
|
|
errorName: "ThrownError",
|
|
},
|
|
],
|
|
],
|
|
TELEMETRY_EVENTS_FILTERS
|
|
);
|
|
|
|
client.db.list = backup;
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(async function test_bucketname_changes_when_preview_mode_is_enabled() {
|
|
equal(client.bucketName, "main");
|
|
|
|
RemoteSettings.enablePreviewMode(true);
|
|
|
|
equal(client.bucketName, "main-preview");
|
|
});
|
|
add_task(clear_state);
|
|
|
|
add_task(
|
|
async function test_preview_mode_pref_affects_bucket_names_before_instantiated() {
|
|
Services.prefs.setBoolPref("services.settings.preview_enabled", true);
|
|
|
|
let clientWithDefaultBucket = RemoteSettings("other");
|
|
let clientWithBucket = RemoteSettings("coll", { bucketName: "buck" });
|
|
|
|
equal(clientWithDefaultBucket.bucketName, "main-preview");
|
|
equal(clientWithBucket.bucketName, "buck-preview");
|
|
}
|
|
);
|
|
add_task(clear_state);
|
|
|
|
add_task(
|
|
async function test_preview_enabled_pref_ignored_when_mode_is_set_explicitly() {
|
|
Services.prefs.setBoolPref("services.settings.preview_enabled", true);
|
|
|
|
let clientWithDefaultBucket = RemoteSettings("other");
|
|
let clientWithBucket = RemoteSettings("coll", { bucketName: "buck" });
|
|
|
|
equal(clientWithDefaultBucket.bucketName, "main-preview");
|
|
equal(clientWithBucket.bucketName, "buck-preview");
|
|
|
|
RemoteSettings.enablePreviewMode(false);
|
|
|
|
equal(clientWithDefaultBucket.bucketName, "main");
|
|
equal(clientWithBucket.bucketName, "buck");
|
|
}
|
|
);
|
|
add_task(clear_state);
|
|
|
|
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_local_db_distinguishes_preview_records() {
|
|
RemoteSettings.enablePreviewMode(true);
|
|
client.db.importChanges({}, Date.now(), [{ id: "record-1" }], {
|
|
clear: true,
|
|
});
|
|
|
|
RemoteSettings.enablePreviewMode(false);
|
|
client.db.importChanges({}, Date.now(), [{ id: "record-2" }], {
|
|
clear: true,
|
|
});
|
|
|
|
deepEqual(await client.get(), [{ id: "record-2" }]);
|
|
});
|
|
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");
|
|
|
|
const { collections: before, previewMode: previewModeBefore } =
|
|
await RemoteSettings.inspect();
|
|
|
|
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);
|
|
|
|
const {
|
|
collections: after,
|
|
mainBucket,
|
|
previewMode,
|
|
} = await RemoteSettings.inspect();
|
|
|
|
Assert.ok(previewMode, "preview is enabled");
|
|
|
|
// 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"]),
|
|
});
|
|
|
|
await client.maybeSync(2000, { trigger: "broadcast" });
|
|
|
|
Assert.ok(
|
|
await client.attachments.cacheImpl.get("bar"),
|
|
"Extra attachment was not deleted on broadcast"
|
|
);
|
|
|
|
await client.maybeSync(3001, { trigger: "timer" });
|
|
|
|
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());
|
|
|
|
const body =
|
|
typeof sample.responseBody == "string"
|
|
? sample.responseBody
|
|
: JSON.stringify(sample.responseBody);
|
|
response.write(body);
|
|
response.finish();
|
|
} catch (e) {
|
|
info(e);
|
|
}
|
|
}
|
|
|
|
function getSampleResponse(req, port) {
|
|
const responses = {
|
|
OPTIONS: {
|
|
sampleHeaders: [
|
|
"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",
|
|
"Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
|
|
"Access-Control-Allow-Origin: *",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: null,
|
|
},
|
|
"GET:/v1/": {
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
settings: {
|
|
batch_max_requests: 25,
|
|
},
|
|
url: `http://localhost:${port}/v1/`,
|
|
documentation: "https://kinto.readthedocs.org/",
|
|
version: "1.5.1",
|
|
commit: "cbc6f58",
|
|
hello: "kinto",
|
|
},
|
|
},
|
|
"GET:/v1/buckets/monitor/collections/changes/changeset": {
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
`Date: ${new Date().toUTCString()}`,
|
|
'Etag: "5000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
timestamp: 5000,
|
|
changes: [
|
|
{
|
|
id: "4676f0c7-9757-4796-a0e8-b40a5a37a9c9",
|
|
bucket: "main",
|
|
collection: "unknown-locally",
|
|
last_modified: 5000,
|
|
},
|
|
{
|
|
id: "4676f0c7-9757-4796-a0e8-b40a5a37a9c9",
|
|
bucket: "main",
|
|
collection: "language-dictionaries",
|
|
last_modified: 4000,
|
|
},
|
|
{
|
|
id: "0af8da0b-3e03-48fb-8d0d-2d8e4cb7514d",
|
|
bucket: "main",
|
|
collection: "password-fields",
|
|
last_modified: 3000,
|
|
},
|
|
{
|
|
id: "4acda969-3bd3-4074-a678-ff311eeb076e",
|
|
bucket: "main-preview",
|
|
collection: "password-fields",
|
|
last_modified: 2000,
|
|
},
|
|
{
|
|
id: "58697bd1-315f-4185-9bee-3371befc2585",
|
|
bucket: "main-preview",
|
|
collection: "crash-rate",
|
|
last_modified: 1000,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/fake-x5u": {
|
|
sampleHeaders: ["Content-Type: application/octet-stream"],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: `-----BEGIN CERTIFICATE-----
|
|
MIIGYTCCBEmgAwIBAgIBATANBgkqhkiG9w0BAQwFADB9MQswCQYDVQQGEwJVU
|
|
ZARKjbu1TuYQHf0fs+GwID8zeLc2zJL7UzcHFwwQ6Nda9OJN4uPAuC/BKaIpxCLL
|
|
26b24/tRam4SJjqpiq20lynhUrmTtt6hbG3E1Hpy3bmkt2DYnuMFwEx2gfXNcnbT
|
|
wNuvFqc=
|
|
-----END CERTIFICATE-----`,
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=2000":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "3000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
timestamp: 3000,
|
|
metadata: {
|
|
id: "password-fields",
|
|
last_modified: 1234,
|
|
signature: {
|
|
signature: "abcdef",
|
|
x5u: `http://localhost:${port}/fake-x5u`,
|
|
},
|
|
},
|
|
changes: [
|
|
{
|
|
id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
|
|
last_modified: 3000,
|
|
website: "https://some-website.com",
|
|
selector: "#user[password]",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=%223000%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "4000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
metadata: {
|
|
signature: {},
|
|
},
|
|
timestamp: 4000,
|
|
changes: [
|
|
{
|
|
id: "aabad965-e556-ffe7-4191-074f5dee3df3",
|
|
last_modified: 4000,
|
|
website: "https://www.other.org/signin",
|
|
selector: "#signinpassword",
|
|
},
|
|
{
|
|
id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
|
|
last_modified: 3500,
|
|
website: "https://some-website.com/login",
|
|
selector: "input#user[password]",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=%224000%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "5000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
metadata: {
|
|
signature: {},
|
|
},
|
|
timestamp: 5000,
|
|
changes: [
|
|
{
|
|
id: "aabad965-e556-ffe7-4191-074f5dee3df3",
|
|
deleted: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=%229999%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
],
|
|
status: { status: 503, statusText: "Service Unavailable" },
|
|
responseBody: {
|
|
code: 503,
|
|
errno: 999,
|
|
error: "Service Unavailable",
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=%2210000%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "10001"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: "<invalid json",
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=%2211000%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
],
|
|
status: { status: 503, statusText: "Service Unavailable" },
|
|
responseBody: {
|
|
changes: [
|
|
{
|
|
id: "c4f021e3-f68c-4269-ad2a-d4ba87762b35",
|
|
last_modified: 4000,
|
|
website: "https://www.eff.org",
|
|
selector: "#pwd",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields?_expected=11001": {
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
],
|
|
status: { status: 503, statusText: "Service Unavailable" },
|
|
responseBody: {
|
|
code: 503,
|
|
errno: 999,
|
|
error: "Service Unavailable",
|
|
},
|
|
},
|
|
"GET:/v1/buckets/monitor/collections/changes/changeset?collection=password-fields&bucket=main&_expected=0":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
`Date: ${new Date().toUTCString()}`,
|
|
'Etag: "1338"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
timestamp: 1338,
|
|
changes: [
|
|
{
|
|
id: "fe5758d0-c67a-42d0-bb4f-8f2d75106b65",
|
|
bucket: "main",
|
|
collection: "password-fields",
|
|
last_modified: 1337,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "3000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
metadata: {
|
|
signature: {
|
|
signature: "some-sig",
|
|
x5u: `http://localhost:${port}/fake-x5u`,
|
|
},
|
|
},
|
|
timestamp: 3000,
|
|
changes: [
|
|
{
|
|
id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69",
|
|
last_modified: 3000,
|
|
website: "https://some-website.com",
|
|
selector: "#webpage[field-pwd]",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=%223000%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "3001"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
metadata: {
|
|
signature: {
|
|
signature: "some-sig",
|
|
x5u: `http://localhost:${port}/fake-x5u`,
|
|
},
|
|
},
|
|
timestamp: 3001,
|
|
changes: [
|
|
{
|
|
id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69",
|
|
last_modified: 3001,
|
|
website: "https://some-website-2.com",
|
|
selector: "#webpage[field-pwd]",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/language-dictionaries/changeset": {
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "5000000000000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
timestamp: 5000000000000,
|
|
metadata: {
|
|
id: "language-dictionaries",
|
|
last_modified: 1234,
|
|
signature: {
|
|
signature: "xyz",
|
|
x5u: `http://localhost:${port}/fake-x5u`,
|
|
},
|
|
},
|
|
changes: [
|
|
{
|
|
id: "xx",
|
|
last_modified: 5000000000000,
|
|
dictionaries: ["xx-XX@dictionaries.addons.mozilla.org"],
|
|
},
|
|
{
|
|
id: "fr",
|
|
last_modified: 5000000000000 - 1,
|
|
deleted: true,
|
|
},
|
|
{
|
|
id: "pt-BR",
|
|
last_modified: 5000000000000 - 2,
|
|
dictionaries: ["pt-BR@for-tests"],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=2000":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "2000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
timestamp: 2000,
|
|
metadata: {
|
|
id: "with-local-fields",
|
|
last_modified: 1234,
|
|
signature: {
|
|
signature: "xyz",
|
|
x5u: `http://localhost:${port}/fake-x5u`,
|
|
},
|
|
},
|
|
changes: [
|
|
{
|
|
id: "c74279ce-fb0a-42a6-ae11-386b567a6119",
|
|
last_modified: 2000,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=%222000%22":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
'Etag: "3000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
timestamp: 3000,
|
|
metadata: {
|
|
signature: {},
|
|
},
|
|
changes: [
|
|
{
|
|
id: "1f5c98b9-6d93-4c13-aa26-978b38695096",
|
|
last_modified: 3000,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"GET:/v1/buckets/monitor/collections/changes/changeset?collection=no-mocked-responses&bucket=main&_expected=0":
|
|
{
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
`Date: ${new Date().toUTCString()}`,
|
|
'Etag: "713705"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: {
|
|
data: [
|
|
{
|
|
id: "07a98d1b-7c62-4344-ab18-76856b3facd8",
|
|
bucket: "main",
|
|
collection: "no-mocked-responses",
|
|
last_modified: 713705,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
return (
|
|
responses[`${req.method}:${req.path}?${req.queryString}`] ||
|
|
responses[`${req.method}:${req.path}`] ||
|
|
responses[req.method]
|
|
);
|
|
}
|
|
|
|
add_task(clear_state);
|
|
|
|
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");
|
|
|
|
await client.db.saveAttachment("foo", {
|
|
record: { id: "foo" },
|
|
blob: new Blob(["foo"]),
|
|
});
|
|
|
|
res = await client.db.hasAttachments();
|
|
Assert.equal(res, true, "Should return true, just saved an attachment");
|
|
|
|
await client.db.pruneAttachments([]);
|
|
|
|
res = await client.db.hasAttachments();
|
|
Assert.equal(res, false, "Should return false after attachments are pruned");
|
|
});
|