Bug 1616052 - Add low-level helpers to debug and test Remote Settings r=tarek,glasserc

Differential Revision: https://phabricator.services.mozilla.com/D63207

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mathieu Leplatre 2020-02-25 15:29:48 +00:00
parent 234f641a3d
commit 38a37caab2
6 changed files with 134 additions and 27 deletions

View File

@ -142,7 +142,6 @@ The provided helper will:
The following aspects are not taken care of (yet! help welcome):
- report Telemetry when download fails
- check available disk space
- preserve bandwidth
- resume downloads of large files
@ -295,6 +294,12 @@ The synchronization of every known remote settings clients can be triggered manu
await RemoteSettings.pollChanges()
In order to ignore last synchronization status during polling for changes, set the ``full`` option:
.. code-block:: js
await RemoteSettings.pollChanges({ full: true })
The synchronization of a single client can be forced with the ``.sync()`` method:
.. code-block:: js
@ -314,6 +319,16 @@ The internal IndexedDB of Remote Settings can be accessed via the Storage Inspec
For example, the local data of the ``"key"`` collection can be accessed in the ``remote-settings`` database at *Browser Toolbox* > *Storage* > *IndexedDB* > *chrome*, in the ``records`` store.
Delete all local data
---------------------
All local data, of **every collection**, including downloaded attachments, can be deleted with:
.. code-block:: js
await RemoteSettings.clearAll();
Unit Tests
==========

View File

@ -171,6 +171,21 @@ class AttachmentDownloader extends Downloader {
throw err;
}
}
/**
* Delete all downloaded records attachments.
*
* Note: the list of attachments to be deleted is based on the
* current list of records.
*/
async deleteAll() {
const kintoCol = await this._client.openCollection();
const { data: allRecords } = await kintoCol.list();
await kintoCol.db.close();
return Promise.all(
allRecords.filter(r => !!r.attachment).map(r => this.delete(r))
);
}
}
class RemoteSettingsClient extends EventEmitter {
@ -263,7 +278,6 @@ class RemoteSettingsClient extends EventEmitter {
*/
async getLastModified() {
let timestamp = -1;
try {
const collection = await this.openCollection();
timestamp = await collection.db.getLastModified();

View File

@ -17,31 +17,15 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"UptakeTelemetry",
"resource://services-common/uptake-telemetry.js"
);
ChromeUtils.defineModuleGetter(
this,
"pushBroadcastService",
"resource://gre/modules/PushBroadcastService.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"RemoteSettingsClient",
"resource://services-settings/RemoteSettingsClient.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Utils",
"resource://services-settings/Utils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FilterExpressions",
"resource://gre/modules/components-utils/FilterExpressions.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
UptakeTelemetry: "resource://services-common/uptake-telemetry.js",
pushBroadcastService: "resource://gre/modules/PushBroadcastService.jsm",
RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
Utils: "resource://services-settings/Utils.jsm",
FilterExpressions:
"resource://gre/modules/components-utils/FilterExpressions.jsm",
RemoteSettingsWorker: "resource://services-settings/RemoteSettingsWorker.jsm",
});
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
@ -171,12 +155,21 @@ function remoteSettingsFunction() {
* @param {Object} options
. * @param {Object} options.expectedTimestamp (optional) The expected timestamp to be received used by servers for cache busting.
* @param {string} options.trigger (optional) label to identify what triggered this sync (eg. ``"timer"``, default: `"manual"`)
* @param {bool} options.full (optional) Ignore last polling status and fetch all changes (default: `false`)
* @returns {Promise} or throws error if something goes wrong.
*/
remoteSettings.pollChanges = async ({
expectedTimestamp,
trigger = "manual",
full = false,
} = {}) => {
// When running in full mode, we ignore last polling status.
if (full) {
gPrefs.clearUserPref(PREF_SETTINGS_SERVER_BACKOFF);
gPrefs.clearUserPref(PREF_SETTINGS_LAST_UPDATE);
gPrefs.clearUserPref(PREF_SETTINGS_LAST_ETAG);
}
let pollTelemetryArgs = {
source: TELEMETRY_SOURCE_POLL,
trigger,
@ -419,6 +412,26 @@ function remoteSettingsFunction() {
};
};
/**
* Delete all local data, of every collection.
*/
remoteSettings.clearAll = async () => {
const { collections } = await remoteSettings.inspect();
await Promise.all(
collections.map(async ({ collection }) => {
const client = RemoteSettings(collection);
// Delete all potential attachments.
await client.attachments.deleteAll();
// Delete local data.
const kintoCollection = await client.openCollection();
await kintoCollection.clear();
await kintoCollection.db.close();
// Remove status pref.
Services.prefs.clearUserPref(client.lastCheckTimePref);
})
);
};
/**
* Startup function called from nsBrowserGlue.
*/

View File

@ -196,6 +196,20 @@ add_task(async function test_delete_removes_local_file() {
});
add_task(clear_state);
add_task(async function test_delete_all() {
const client = RemoteSettings("some-collection");
const kintoCol = await client.openCollection();
await kintoCol.create(RECORD);
const fileURL = await downloader.download(RECORD);
const localFilePath = pathFromURL(fileURL);
Assert.ok(await OS.File.exists(localFilePath));
await client.attachments.deleteAll();
Assert.ok(!(await OS.File.exists(localFilePath)));
});
add_task(clear_state);
add_task(async function test_downloader_is_accessible_via_client() {
const client = RemoteSettings("some-collection");

View File

@ -269,6 +269,7 @@ add_task(async function test_get_does_not_sync_if_empty_dump_is_provided() {
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 });
@ -501,6 +502,28 @@ add_task(async function test_inspect_method() {
});
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 = () => {

View File

@ -538,6 +538,34 @@ add_task(async function test_success_with_partial_list() {
});
add_task(clear_state);
add_task(async function test_full_polling() {
server.registerPathHandler(
CHANGES_PATH,
serveChangesEntries(10000, [
{
id: "b6ba7fab-a40a-4d03-a4af-6b627f3c5b36",
last_modified: 42,
host: "localhost",
bucket: "main",
collection: "poll-test-collection",
},
])
);
const c = RemoteSettings("poll-test-collection");
let maybeSyncCount = 0;
c.maybeSync = () => {
maybeSyncCount++;
};
await RemoteSettings.pollChanges();
await RemoteSettings.pollChanges({ full: true });
// Since the second call is full, clients are called
Assert.equal(maybeSyncCount, 2, "maybeSync should be called twice");
});
add_task(clear_state);
add_task(async function test_server_bad_json() {
const startHistogram = getUptakeTelemetrySnapshot(
TELEMETRY_HISTOGRAM_POLL_KEY