1462 lines
44 KiB
JavaScript
1462 lines
44 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
const { Service } = ChromeUtils.importESModule(
|
|
"resource://services-sync/service.sys.mjs"
|
|
);
|
|
const { WBORecord } = ChromeUtils.importESModule(
|
|
"resource://services-sync/record.sys.mjs"
|
|
);
|
|
const { Resource } = ChromeUtils.importESModule(
|
|
"resource://services-sync/resource.sys.mjs"
|
|
);
|
|
const { RotaryEngine } = ChromeUtils.importESModule(
|
|
"resource://testing-common/services/sync/rotaryengine.sys.mjs"
|
|
);
|
|
const { getFxAccountsSingleton } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/FxAccounts.sys.mjs"
|
|
);
|
|
const fxAccounts = getFxAccountsSingleton();
|
|
|
|
function SteamStore(engine) {
|
|
Store.call(this, "Steam", engine);
|
|
}
|
|
Object.setPrototypeOf(SteamStore.prototype, Store.prototype);
|
|
|
|
function SteamTracker(name, engine) {
|
|
LegacyTracker.call(this, name || "Steam", engine);
|
|
}
|
|
Object.setPrototypeOf(SteamTracker.prototype, LegacyTracker.prototype);
|
|
|
|
function SteamEngine(service) {
|
|
SyncEngine.call(this, "steam", service);
|
|
}
|
|
|
|
SteamEngine.prototype = {
|
|
_storeObj: SteamStore,
|
|
_trackerObj: SteamTracker,
|
|
_errToThrow: null,
|
|
problemsToReport: null,
|
|
async _sync() {
|
|
if (this._errToThrow) {
|
|
throw this._errToThrow;
|
|
}
|
|
},
|
|
getValidator() {
|
|
return new SteamValidator();
|
|
},
|
|
};
|
|
Object.setPrototypeOf(SteamEngine.prototype, SyncEngine.prototype);
|
|
|
|
function BogusEngine(service) {
|
|
SyncEngine.call(this, "bogus", service);
|
|
}
|
|
|
|
BogusEngine.prototype = Object.create(SteamEngine.prototype);
|
|
|
|
class SteamValidator {
|
|
async canValidate() {
|
|
return true;
|
|
}
|
|
|
|
async validate(engine) {
|
|
return {
|
|
problems: new SteamValidationProblemData(engine.problemsToReport),
|
|
version: 1,
|
|
duration: 0,
|
|
recordCount: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
class SteamValidationProblemData {
|
|
constructor(problemsToReport = []) {
|
|
this.problemsToReport = problemsToReport;
|
|
}
|
|
|
|
getSummary() {
|
|
return this.problemsToReport;
|
|
}
|
|
}
|
|
|
|
async function cleanAndGo(engine, server) {
|
|
await engine._tracker.clearChangedIDs();
|
|
for (const pref of Svc.PrefBranch.getChildList("")) {
|
|
Svc.PrefBranch.clearUserPref(pref);
|
|
}
|
|
syncTestLogging();
|
|
Service.recordManager.clearCache();
|
|
await promiseStopServer(server);
|
|
}
|
|
|
|
add_task(async function setup() {
|
|
// Avoid addon manager complaining about not being initialized
|
|
await Service.engineManager.unregister("addons");
|
|
await Service.engineManager.unregister("extension-storage");
|
|
});
|
|
|
|
add_task(async function test_basic() {
|
|
enableValidationPrefs();
|
|
|
|
let helper = track_collections_helper();
|
|
let upd = helper.with_updated_collection;
|
|
|
|
let handlers = {
|
|
"/1.1/johndoe/info/collections": helper.handler,
|
|
"/1.1/johndoe/storage/crypto/keys": upd(
|
|
"crypto",
|
|
new ServerWBO("keys").handler()
|
|
),
|
|
"/1.1/johndoe/storage/meta/global": upd(
|
|
"meta",
|
|
new ServerWBO("global").handler()
|
|
),
|
|
};
|
|
|
|
let collections = [
|
|
"clients",
|
|
"bookmarks",
|
|
"forms",
|
|
"history",
|
|
"passwords",
|
|
"prefs",
|
|
"tabs",
|
|
];
|
|
|
|
for (let coll of collections) {
|
|
handlers["/1.1/johndoe/storage/" + coll] = upd(
|
|
coll,
|
|
new ServerCollection({}, true).handler()
|
|
);
|
|
}
|
|
|
|
let server = httpd_setup(handlers);
|
|
await configureIdentity({ username: "johndoe" }, server);
|
|
|
|
let ping = await wait_for_ping(() => Service.sync(), true, true);
|
|
|
|
// Check the "os" block - we can't really check specific values, but can
|
|
// check it smells sane.
|
|
ok(ping.os, "there is an OS block");
|
|
ok("name" in ping.os, "there is an OS name");
|
|
ok("version" in ping.os, "there is an OS version");
|
|
ok("locale" in ping.os, "there is an OS locale");
|
|
|
|
for (const pref of Svc.PrefBranch.getChildList("")) {
|
|
Svc.PrefBranch.clearUserPref(pref);
|
|
}
|
|
await promiseStopServer(server);
|
|
});
|
|
|
|
add_task(async function test_processIncoming_error() {
|
|
let engine = Service.engineManager.get("bookmarks");
|
|
await engine.initialize();
|
|
let store = engine._store;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
let collection = server.user("foo").collection("bookmarks");
|
|
try {
|
|
// Create a bogus record that when synced down will provoke a
|
|
// network error which in turn provokes an exception in _processIncoming.
|
|
const BOGUS_GUID = "zzzzzzzzzzzz";
|
|
let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
|
|
bogus_record.get = function get() {
|
|
throw new Error("Sync this!");
|
|
};
|
|
// Make the 10 minutes old so it will only be synced in the toFetch phase.
|
|
bogus_record.modified = Date.now() / 1000 - 60 * 10;
|
|
await engine.setLastSync(Date.now() / 1000 - 60);
|
|
engine.toFetch = new SerializableSet([BOGUS_GUID]);
|
|
|
|
let error, pingPayload, fullPing;
|
|
try {
|
|
await sync_engine_and_validate_telem(
|
|
engine,
|
|
true,
|
|
(errPing, fullErrPing) => {
|
|
pingPayload = errPing;
|
|
fullPing = fullErrPing;
|
|
}
|
|
);
|
|
} catch (ex) {
|
|
error = ex;
|
|
}
|
|
ok(!!error);
|
|
ok(!!pingPayload);
|
|
|
|
equal(fullPing.uid, "f".repeat(32)); // as setup by SyncTestingInfrastructure
|
|
deepEqual(pingPayload.failureReason, {
|
|
name: "httperror",
|
|
code: 500,
|
|
});
|
|
|
|
equal(pingPayload.engines.length, 1);
|
|
|
|
equal(pingPayload.engines[0].name, "bookmarks-buffered");
|
|
deepEqual(pingPayload.engines[0].failureReason, {
|
|
name: "httperror",
|
|
code: 500,
|
|
});
|
|
} finally {
|
|
await store.wipe();
|
|
await cleanAndGo(engine, server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_uploading() {
|
|
let engine = Service.engineManager.get("bookmarks");
|
|
await engine.initialize();
|
|
let store = engine._store;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let bmk = await PlacesUtils.bookmarks.insert({
|
|
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
|
|
url: "http://getfirefox.com/",
|
|
title: "Get IceCat!",
|
|
});
|
|
|
|
try {
|
|
let ping = await sync_engine_and_validate_telem(engine, false);
|
|
ok(!!ping);
|
|
equal(ping.engines.length, 1);
|
|
equal(ping.engines[0].name, "bookmarks-buffered");
|
|
ok(!!ping.engines[0].outgoing);
|
|
greater(ping.engines[0].outgoing[0].sent, 0);
|
|
ok(!ping.engines[0].incoming);
|
|
|
|
await PlacesUtils.bookmarks.update({
|
|
guid: bmk.guid,
|
|
title: "New Title",
|
|
});
|
|
|
|
await store.wipe();
|
|
await engine.resetClient();
|
|
// We don't sync via the service, so don't re-hit info/collections, so
|
|
// lastModified remaning at zero breaks things subtly...
|
|
engine.lastModified = null;
|
|
|
|
ping = await sync_engine_and_validate_telem(engine, false);
|
|
equal(ping.engines.length, 1);
|
|
equal(ping.engines[0].name, "bookmarks-buffered");
|
|
equal(ping.engines[0].outgoing.length, 1);
|
|
ok(!!ping.engines[0].incoming);
|
|
} finally {
|
|
// Clean up.
|
|
await store.wipe();
|
|
await cleanAndGo(engine, server);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_upload_failed() {
|
|
let collection = new ServerCollection();
|
|
collection._wbos.flying = new ServerWBO("flying");
|
|
|
|
let server = sync_httpd_setup({
|
|
"/1.1/foo/storage/rotary": collection.handler(),
|
|
});
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
await configureIdentity({ username: "foo" }, server);
|
|
|
|
let engine = new RotaryEngine(Service);
|
|
engine._store.items = {
|
|
flying: "LNER Class A3 4472",
|
|
scotsman: "Flying Scotsman",
|
|
peppercorn: "Peppercorn Class",
|
|
};
|
|
const FLYING_CHANGED = 12345;
|
|
const SCOTSMAN_CHANGED = 23456;
|
|
const PEPPERCORN_CHANGED = 34567;
|
|
await engine._tracker.addChangedID("flying", FLYING_CHANGED);
|
|
await engine._tracker.addChangedID("scotsman", SCOTSMAN_CHANGED);
|
|
await engine._tracker.addChangedID("peppercorn", PEPPERCORN_CHANGED);
|
|
|
|
let syncID = await engine.resetLocalSyncID();
|
|
let meta_global = Service.recordManager.set(
|
|
engine.metaURL,
|
|
new WBORecord(engine.metaURL)
|
|
);
|
|
meta_global.payload.engines = { rotary: { version: engine.version, syncID } };
|
|
|
|
try {
|
|
await engine.setLastSync(123); // needs to be non-zero so that tracker is queried
|
|
let changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_upload_failed: Rotary tracker contents at first sync: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
engine.enabled = true;
|
|
let ping = await sync_engine_and_validate_telem(engine, true);
|
|
ok(!!ping);
|
|
equal(ping.engines.length, 1);
|
|
equal(ping.engines[0].incoming, null);
|
|
deepEqual(ping.engines[0].outgoing, [
|
|
{
|
|
sent: 3,
|
|
failed: 2,
|
|
failedReasons: [
|
|
{ name: "scotsman", count: 1 },
|
|
{ name: "peppercorn", count: 1 },
|
|
],
|
|
},
|
|
]);
|
|
await engine.setLastSync(123);
|
|
|
|
changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_upload_failed: Rotary tracker contents at second sync: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
ping = await sync_engine_and_validate_telem(engine, true);
|
|
ok(!!ping);
|
|
equal(ping.engines.length, 1);
|
|
deepEqual(ping.engines[0].outgoing, [
|
|
{
|
|
sent: 2,
|
|
failed: 2,
|
|
failedReasons: [
|
|
{ name: "scotsman", count: 1 },
|
|
{ name: "peppercorn", count: 1 },
|
|
],
|
|
},
|
|
]);
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await engine.finalize();
|
|
}
|
|
});
|
|
|
|
add_task(async function test_sync_partialUpload() {
|
|
let collection = new ServerCollection();
|
|
let server = sync_httpd_setup({
|
|
"/1.1/foo/storage/rotary": collection.handler(),
|
|
});
|
|
await SyncTestingInfrastructure(server);
|
|
await generateNewKeys(Service.collectionKeys);
|
|
|
|
let engine = new RotaryEngine(Service);
|
|
await engine.setLastSync(123);
|
|
|
|
// Create a bunch of records (and server side handlers)
|
|
for (let i = 0; i < 234; i++) {
|
|
let id = "record-no-" + i;
|
|
engine._store.items[id] = "Record No. " + i;
|
|
await engine._tracker.addChangedID(id, i);
|
|
// Let two items in the first upload batch fail.
|
|
if (i != 23 && i != 42) {
|
|
collection.insert(id);
|
|
}
|
|
}
|
|
|
|
let syncID = await engine.resetLocalSyncID();
|
|
let meta_global = Service.recordManager.set(
|
|
engine.metaURL,
|
|
new WBORecord(engine.metaURL)
|
|
);
|
|
meta_global.payload.engines = { rotary: { version: engine.version, syncID } };
|
|
|
|
try {
|
|
let changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_sync_partialUpload: Rotary tracker contents at first sync: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
engine.enabled = true;
|
|
let ping = await sync_engine_and_validate_telem(engine, true);
|
|
|
|
ok(!!ping);
|
|
ok(!ping.failureReason);
|
|
equal(ping.engines.length, 1);
|
|
equal(ping.engines[0].name, "rotary");
|
|
ok(!ping.engines[0].incoming);
|
|
ok(!ping.engines[0].failureReason);
|
|
deepEqual(ping.engines[0].outgoing, [
|
|
{
|
|
sent: 234,
|
|
failed: 2,
|
|
failedReasons: [
|
|
{ name: "record-no-23", count: 1 },
|
|
{ name: "record-no-42", count: 1 },
|
|
],
|
|
},
|
|
]);
|
|
collection.post = function () {
|
|
throw new Error("Failure");
|
|
};
|
|
|
|
engine._store.items["record-no-1000"] = "Record No. 1000";
|
|
await engine._tracker.addChangedID("record-no-1000", 1000);
|
|
collection.insert("record-no-1000", 1000);
|
|
|
|
await engine.setLastSync(123);
|
|
ping = null;
|
|
|
|
changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_sync_partialUpload: Rotary tracker contents at second sync: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
try {
|
|
// should throw
|
|
await sync_engine_and_validate_telem(
|
|
engine,
|
|
true,
|
|
errPing => (ping = errPing)
|
|
);
|
|
} catch (e) {}
|
|
// It would be nice if we had a more descriptive error for this...
|
|
let uploadFailureError = {
|
|
name: "httperror",
|
|
code: 500,
|
|
};
|
|
|
|
ok(!!ping);
|
|
deepEqual(ping.failureReason, uploadFailureError);
|
|
equal(ping.engines.length, 1);
|
|
equal(ping.engines[0].name, "rotary");
|
|
deepEqual(ping.engines[0].incoming, {
|
|
failed: 1,
|
|
failedReasons: [{ name: "No ciphertext: nothing to decrypt?", count: 1 }],
|
|
});
|
|
ok(!ping.engines[0].outgoing);
|
|
deepEqual(ping.engines[0].failureReason, uploadFailureError);
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await engine.finalize();
|
|
}
|
|
});
|
|
|
|
add_task(async function test_generic_engine_fail() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
let e = new Error("generic failure message");
|
|
engine._errToThrow = e;
|
|
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_generic_engine_fail: Steam tracker contents: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
deepEqual(ping.engines.find(err => err.name === "steam").failureReason, {
|
|
name: "unexpectederror",
|
|
error: String(e),
|
|
});
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_engine_fail_weird_errors() {
|
|
enableValidationPrefs();
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
let msg = "Bad things happened!";
|
|
engine._errToThrow = { message: msg };
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
deepEqual(ping.engines.find(err => err.name === "steam").failureReason, {
|
|
name: "unexpectederror",
|
|
error: "Bad things happened!",
|
|
});
|
|
});
|
|
let e = { msg };
|
|
engine._errToThrow = e;
|
|
await sync_and_validate_telem(ping => {
|
|
deepEqual(ping.engines.find(err => err.name === "steam").failureReason, {
|
|
name: "unexpectederror",
|
|
error: JSON.stringify(e),
|
|
});
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_overrideTelemetryName() {
|
|
enableValidationPrefs(["steam"]);
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.overrideTelemetryName = "steam-but-better";
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
const problemsToReport = [
|
|
{ name: "someProblem", count: 123 },
|
|
{ name: "anotherProblem", count: 456 },
|
|
];
|
|
|
|
try {
|
|
info("Sync with validation problems");
|
|
engine.problemsToReport = problemsToReport;
|
|
await sync_and_validate_telem(ping => {
|
|
let enginePing = ping.engines.find(e => e.name === "steam-but-better");
|
|
ok(enginePing);
|
|
ok(!ping.engines.find(e => e.name === "steam"));
|
|
deepEqual(
|
|
enginePing.validation,
|
|
{
|
|
version: 1,
|
|
checked: 0,
|
|
problems: problemsToReport,
|
|
},
|
|
"Should include validation report with overridden name"
|
|
);
|
|
});
|
|
|
|
info("Sync without validation problems");
|
|
engine.problemsToReport = null;
|
|
await sync_and_validate_telem(ping => {
|
|
let enginePing = ping.engines.find(e => e.name === "steam-but-better");
|
|
ok(enginePing);
|
|
ok(!ping.engines.find(e => e.name === "steam"));
|
|
ok(
|
|
!enginePing.validation,
|
|
"Should not include validation report when there are no problems"
|
|
);
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_engine_fail_ioerror() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
// create an IOError to re-throw as part of Sync.
|
|
try {
|
|
// (Note that fakeservices.js has replaced Utils.jsonMove etc, but for
|
|
// this test we need the real one so we get real exceptions from the
|
|
// filesystem.)
|
|
await Utils._real_jsonMove("file-does-not-exist", "anything", {});
|
|
} catch (ex) {
|
|
engine._errToThrow = ex;
|
|
}
|
|
ok(engine._errToThrow, "expecting exception");
|
|
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_engine_fail_ioerror: Steam tracker contents: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
let failureReason = ping.engines.find(
|
|
e => e.name === "steam"
|
|
).failureReason;
|
|
equal(failureReason.name, "unexpectederror");
|
|
// ensure the profile dir in the exception message has been stripped.
|
|
ok(
|
|
!failureReason.error.includes(PathUtils.profileDir),
|
|
failureReason.error
|
|
);
|
|
ok(failureReason.error.includes("[profileDir]"), failureReason.error);
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_error_detections() {
|
|
let telem = get_sync_test_telemetry();
|
|
|
|
// Non-network NS_ERROR_ codes get their own category.
|
|
Assert.deepEqual(
|
|
telem.transformError(Components.Exception("", Cr.NS_ERROR_FAILURE)),
|
|
{ name: "nserror", code: Cr.NS_ERROR_FAILURE }
|
|
);
|
|
|
|
// Some NS_ERROR_ code in the "network" module are treated as http errors.
|
|
Assert.deepEqual(
|
|
telem.transformError(Components.Exception("", Cr.NS_ERROR_UNKNOWN_HOST)),
|
|
{ name: "httperror", code: Cr.NS_ERROR_UNKNOWN_HOST }
|
|
);
|
|
// Some NS_ERROR_ABORT is treated as network by our telemetry.
|
|
Assert.deepEqual(
|
|
telem.transformError(Components.Exception("", Cr.NS_ERROR_ABORT)),
|
|
{ name: "httperror", code: Cr.NS_ERROR_ABORT }
|
|
);
|
|
});
|
|
|
|
add_task(async function test_clean_urls() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
engine._errToThrow = new TypeError(
|
|
"http://www.google .com is not a valid URL."
|
|
);
|
|
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`);
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
let failureReason = ping.engines.find(
|
|
e => e.name === "steam"
|
|
).failureReason;
|
|
equal(failureReason.name, "unexpectederror");
|
|
equal(failureReason.error, "<URL> is not a valid URL.");
|
|
});
|
|
// Handle other errors that include urls.
|
|
engine._errToThrow =
|
|
"Other error message that includes some:url/foo/bar/ in it.";
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
let failureReason = ping.engines.find(
|
|
e => e.name === "steam"
|
|
).failureReason;
|
|
equal(failureReason.name, "unexpectederror");
|
|
equal(
|
|
failureReason.error,
|
|
"Other error message that includes <URL> in it."
|
|
);
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
// Test sanitizing guid-related errors with the pattern of <guid: {guid}>
|
|
add_task(async function test_sanitize_bookmarks_guid() {
|
|
let { ErrorSanitizer } = ChromeUtils.importESModule(
|
|
"resource://services-sync/telemetry.sys.mjs"
|
|
);
|
|
|
|
for (let [original, expected] of [
|
|
[
|
|
"Can't insert Bookmark <guid: sknD84IdnSY2> into Folder <guid: odfninDdi93_3>",
|
|
"Can't insert Bookmark <GUID> into Folder <GUID>",
|
|
],
|
|
[
|
|
"Merge Error: Item <guid: H6fmPA16gZs9> can't contain itself",
|
|
"Merge Error: Item <GUID> can't contain itself",
|
|
],
|
|
]) {
|
|
const sanitized = ErrorSanitizer.cleanErrorMessage(original);
|
|
Assert.equal(sanitized, expected);
|
|
}
|
|
});
|
|
|
|
// Test sanitization of some hard-coded error strings.
|
|
add_task(async function test_clean_errors() {
|
|
let { ErrorSanitizer } = ChromeUtils.importESModule(
|
|
"resource://services-sync/telemetry.sys.mjs"
|
|
);
|
|
|
|
for (let [message, name, expected] of [
|
|
[
|
|
`Could not open the file at ${PathUtils.join(
|
|
PathUtils.profileDir,
|
|
"weave",
|
|
"addonsreconciler.json"
|
|
)} for writing`,
|
|
"NotFoundError",
|
|
"OS error [File/Path not found] Could not open the file at [profileDir]/weave/addonsreconciler.json for writing",
|
|
],
|
|
[
|
|
`Could not get info for the file at ${PathUtils.join(
|
|
PathUtils.profileDir,
|
|
"weave",
|
|
"addonsreconciler.json"
|
|
)}`,
|
|
"NotAllowedError",
|
|
"OS error [Permission denied] Could not get info for the file at [profileDir]/weave/addonsreconciler.json",
|
|
],
|
|
]) {
|
|
const error = new DOMException(message, name);
|
|
const sanitized = ErrorSanitizer.cleanErrorMessage(message, error);
|
|
Assert.equal(sanitized, expected);
|
|
}
|
|
});
|
|
|
|
// Arrange for a sync to hit a "real" OS error during a sync and make sure it's sanitized.
|
|
add_task(async function test_clean_real_os_error() {
|
|
enableValidationPrefs();
|
|
|
|
// Simulate a real error.
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
let path = PathUtils.join(PathUtils.profileDir, "no", "such", "path.json");
|
|
try {
|
|
await IOUtils.readJSON(path);
|
|
throw new Error("should fail to read the file");
|
|
} catch (ex) {
|
|
engine._errToThrow = ex;
|
|
}
|
|
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`);
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
let failureReason = ping.engines.find(
|
|
e => e.name === "steam"
|
|
).failureReason;
|
|
equal(failureReason.name, "unexpectederror");
|
|
equal(
|
|
failureReason.error,
|
|
"OS error [File/Path not found] Could not open `[profileDir]/no/such/path.json': file does not exist"
|
|
);
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_initial_sync_engines() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
// These are the only ones who actually have things to sync at startup.
|
|
let telemetryEngineNames = ["clients", "prefs", "tabs", "bookmarks-buffered"];
|
|
let server = await serverForEnginesWithKeys(
|
|
{ foo: "password" },
|
|
["bookmarks", "prefs", "tabs"].map(name => Service.engineManager.get(name))
|
|
);
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_initial_sync_engines: Steam tracker contents: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
let ping = await wait_for_ping(() => Service.sync(), true);
|
|
|
|
equal(ping.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
|
|
equal(ping.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
|
|
|
|
// for the rest we don't care about specifics
|
|
for (let e of ping.engines) {
|
|
if (!telemetryEngineNames.includes(engine.name)) {
|
|
continue;
|
|
}
|
|
greaterOrEqual(e.took, 1);
|
|
ok(!!e.outgoing);
|
|
equal(e.outgoing.length, 1);
|
|
notEqual(e.outgoing[0].sent, undefined);
|
|
equal(e.outgoing[0].failed, undefined);
|
|
equal(e.outgoing[0].failedReasons, undefined);
|
|
}
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_nserror() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
engine._errToThrow = Components.Exception(
|
|
"NS_ERROR_UNKNOWN_HOST",
|
|
Cr.NS_ERROR_UNKNOWN_HOST
|
|
);
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(`test_nserror: Steam tracker contents: ${JSON.stringify(changes)}`);
|
|
await sync_and_validate_telem(ping => {
|
|
deepEqual(ping.status, {
|
|
service: SYNC_FAILED_PARTIAL,
|
|
sync: LOGIN_FAILED_NETWORK_ERROR,
|
|
});
|
|
let enginePing = ping.engines.find(e => e.name === "steam");
|
|
deepEqual(enginePing.failureReason, {
|
|
name: "httperror",
|
|
code: Cr.NS_ERROR_UNKNOWN_HOST,
|
|
});
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_sync_why() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(SteamEngine);
|
|
let engine = Service.engineManager.get("steam");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
await SyncTestingInfrastructure(server);
|
|
let e = new Error("generic failure message");
|
|
engine._errToThrow = e;
|
|
|
|
try {
|
|
const changes = await engine._tracker.getChangedIDs();
|
|
_(
|
|
`test_generic_engine_fail: Steam tracker contents: ${JSON.stringify(
|
|
changes
|
|
)}`
|
|
);
|
|
let ping = await wait_for_ping(
|
|
() => Service.sync({ why: "user" }),
|
|
true,
|
|
false
|
|
);
|
|
_(JSON.stringify(ping));
|
|
equal(ping.why, "user");
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_discarding() {
|
|
enableValidationPrefs();
|
|
|
|
let helper = track_collections_helper();
|
|
let upd = helper.with_updated_collection;
|
|
let telem = get_sync_test_telemetry();
|
|
telem.maxPayloadCount = 2;
|
|
telem.submissionInterval = Infinity;
|
|
let oldSubmit = telem.submit;
|
|
|
|
let server;
|
|
try {
|
|
let handlers = {
|
|
"/1.1/johndoe/info/collections": helper.handler,
|
|
"/1.1/johndoe/storage/crypto/keys": upd(
|
|
"crypto",
|
|
new ServerWBO("keys").handler()
|
|
),
|
|
"/1.1/johndoe/storage/meta/global": upd(
|
|
"meta",
|
|
new ServerWBO("global").handler()
|
|
),
|
|
};
|
|
|
|
let collections = [
|
|
"clients",
|
|
"bookmarks",
|
|
"forms",
|
|
"history",
|
|
"passwords",
|
|
"prefs",
|
|
"tabs",
|
|
];
|
|
|
|
for (let coll of collections) {
|
|
handlers["/1.1/johndoe/storage/" + coll] = upd(
|
|
coll,
|
|
new ServerCollection({}, true).handler()
|
|
);
|
|
}
|
|
|
|
server = httpd_setup(handlers);
|
|
await configureIdentity({ username: "johndoe" }, server);
|
|
telem.submit = p =>
|
|
ok(
|
|
false,
|
|
"Submitted telemetry ping when we should not have" + JSON.stringify(p)
|
|
);
|
|
|
|
for (let i = 0; i < 5; ++i) {
|
|
await Service.sync();
|
|
}
|
|
telem.submit = oldSubmit;
|
|
telem.submissionInterval = -1;
|
|
let ping = await wait_for_ping(() => Service.sync(), true, true); // with this we've synced 6 times
|
|
equal(ping.syncs.length, 2);
|
|
equal(ping.discarded, 4);
|
|
} finally {
|
|
telem.maxPayloadCount = 500;
|
|
telem.submissionInterval = -1;
|
|
telem.submit = oldSubmit;
|
|
if (server) {
|
|
await promiseStopServer(server);
|
|
}
|
|
}
|
|
});
|
|
|
|
add_task(async function test_submit_interval() {
|
|
let telem = get_sync_test_telemetry();
|
|
let oldSubmit = telem.submit;
|
|
let numSubmissions = 0;
|
|
telem.submit = function () {
|
|
numSubmissions += 1;
|
|
};
|
|
|
|
function notify(what, data = null) {
|
|
Svc.Obs.notify(what, JSON.stringify(data));
|
|
}
|
|
|
|
try {
|
|
// submissionInterval is set such that each sync should submit
|
|
notify("weave:service:sync:start", { why: "testing" });
|
|
notify("weave:service:sync:finish");
|
|
Assert.equal(numSubmissions, 1, "should submit this ping due to interval");
|
|
|
|
// As should each event outside of a sync.
|
|
Service.recordTelemetryEvent("object", "method");
|
|
Assert.equal(numSubmissions, 2);
|
|
|
|
// But events while we are syncing should not.
|
|
notify("weave:service:sync:start", { why: "testing" });
|
|
Service.recordTelemetryEvent("object", "method");
|
|
Assert.equal(numSubmissions, 2, "no submission for this event");
|
|
notify("weave:service:sync:finish");
|
|
Assert.equal(numSubmissions, 3, "was submitted after sync finish");
|
|
} finally {
|
|
telem.submit = oldSubmit;
|
|
}
|
|
});
|
|
|
|
add_task(async function test_no_foreign_engines_in_error_ping() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(BogusEngine);
|
|
let engine = Service.engineManager.get("bogus");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
engine._errToThrow = new Error("Oh no!");
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.status.service, SYNC_FAILED_PARTIAL);
|
|
ok(ping.engines.every(e => e.name !== "bogus"));
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_no_foreign_engines_in_success_ping() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(BogusEngine);
|
|
let engine = Service.engineManager.get("bogus");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
await sync_and_validate_telem(ping => {
|
|
ok(ping.engines.every(e => e.name !== "bogus"));
|
|
});
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_events() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(BogusEngine);
|
|
let engine = Service.engineManager.get("bogus");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
|
|
let telem = get_sync_test_telemetry();
|
|
telem.submissionInterval = Infinity;
|
|
|
|
try {
|
|
let serverTime = Resource.serverTime;
|
|
Service.recordTelemetryEvent("object", "method", "value", { foo: "bar" });
|
|
let ping = await wait_for_ping(() => Service.sync(), true, true);
|
|
equal(ping.events.length, 1);
|
|
let [timestamp, category, method, object, value, extra] = ping.events[0];
|
|
ok(typeof timestamp == "number" && timestamp > 0); // timestamp.
|
|
equal(category, "sync");
|
|
equal(method, "method");
|
|
equal(object, "object");
|
|
equal(value, "value");
|
|
deepEqual(extra, { foo: "bar", serverTime: String(serverTime) });
|
|
ping = await wait_for_ping(
|
|
() => {
|
|
// Test with optional values.
|
|
Service.recordTelemetryEvent("object", "method");
|
|
},
|
|
false,
|
|
true
|
|
);
|
|
equal(ping.events.length, 1);
|
|
equal(ping.events[0].length, 4);
|
|
|
|
ping = await wait_for_ping(
|
|
() => {
|
|
Service.recordTelemetryEvent("object", "method", "extra");
|
|
},
|
|
false,
|
|
true
|
|
);
|
|
equal(ping.events.length, 1);
|
|
equal(ping.events[0].length, 5);
|
|
|
|
ping = await wait_for_ping(
|
|
() => {
|
|
Service.recordTelemetryEvent("object", "method", undefined, {
|
|
foo: "bar",
|
|
});
|
|
},
|
|
false,
|
|
true
|
|
);
|
|
equal(ping.events.length, 1);
|
|
equal(ping.events[0].length, 6);
|
|
[timestamp, category, method, object, value, extra] = ping.events[0];
|
|
equal(value, null);
|
|
|
|
// Fake a submission due to shutdown.
|
|
ping = await wait_for_ping(
|
|
() => {
|
|
telem.submissionInterval = Infinity;
|
|
Service.recordTelemetryEvent("object", "method", undefined, {
|
|
foo: "bar",
|
|
});
|
|
telem.finish("shutdown");
|
|
},
|
|
false,
|
|
true
|
|
);
|
|
equal(ping.syncs.length, 0);
|
|
equal(ping.events.length, 1);
|
|
equal(ping.events[0].length, 6);
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_histograms() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(BogusEngine);
|
|
let engine = Service.engineManager.get("bogus");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
let histId = "TELEMETRY_TEST_LINEAR";
|
|
Services.obs.notifyObservers(null, "weave:telemetry:histogram", histId);
|
|
let ping = await wait_for_ping(() => Service.sync(), true, true);
|
|
equal(Object.keys(ping.histograms).length, 1);
|
|
equal(ping.histograms[histId].sum, 0);
|
|
equal(ping.histograms[histId].histogram_type, 1);
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_invalid_events() {
|
|
enableValidationPrefs();
|
|
|
|
await Service.engineManager.register(BogusEngine);
|
|
let engine = Service.engineManager.get("bogus");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
|
|
async function checkNotRecorded(...args) {
|
|
Service.recordTelemetryEvent.call(args);
|
|
let ping = await wait_for_ping(() => Service.sync(), false, true);
|
|
equal(ping.events, undefined);
|
|
}
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
let long21 = "l".repeat(21);
|
|
let long81 = "l".repeat(81);
|
|
let long86 = "l".repeat(86);
|
|
await checkNotRecorded("object");
|
|
await checkNotRecorded("object", 2);
|
|
await checkNotRecorded(2, "method");
|
|
await checkNotRecorded("object", "method", 2);
|
|
await checkNotRecorded("object", "method", "value", 2);
|
|
await checkNotRecorded("object", "method", "value", { foo: 2 });
|
|
await checkNotRecorded(long21, "method", "value");
|
|
await checkNotRecorded("object", long21, "value");
|
|
await checkNotRecorded("object", "method", long81);
|
|
let badextra = {};
|
|
badextra[long21] = "x";
|
|
await checkNotRecorded("object", "method", "value", badextra);
|
|
badextra = { x: long86 };
|
|
await checkNotRecorded("object", "method", "value", badextra);
|
|
for (let i = 0; i < 10; i++) {
|
|
badextra["name" + i] = "x";
|
|
}
|
|
await checkNotRecorded("object", "method", "value", badextra);
|
|
} finally {
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_no_ping_for_self_hosters() {
|
|
enableValidationPrefs();
|
|
|
|
let telem = get_sync_test_telemetry();
|
|
let oldSubmit = telem.submit;
|
|
|
|
await Service.engineManager.register(BogusEngine);
|
|
let engine = Service.engineManager.get("bogus");
|
|
engine.enabled = true;
|
|
let server = await serverForFoo(engine);
|
|
|
|
await SyncTestingInfrastructure(server);
|
|
try {
|
|
let submitPromise = new Promise(resolve => {
|
|
telem.submit = function () {
|
|
let result = oldSubmit.apply(this, arguments);
|
|
resolve(result);
|
|
};
|
|
});
|
|
await Service.sync();
|
|
let pingSubmitted = await submitPromise;
|
|
// The Sync testing infrastructure already sets up a custom token server,
|
|
// so we don't need to do anything to simulate a self-hosted user.
|
|
ok(!pingSubmitted, "Should not submit ping with custom token server URL");
|
|
} finally {
|
|
telem.submit = oldSubmit;
|
|
await cleanAndGo(engine, server);
|
|
await Service.engineManager.unregister(engine);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_fxa_device_telem() {
|
|
let t = get_sync_test_telemetry();
|
|
let syncEnabled = true;
|
|
let oldGetClientsEngineRecords = t.getClientsEngineRecords;
|
|
let oldGetFxaDevices = t.getFxaDevices;
|
|
let oldSyncIsEnabled = t.syncIsEnabled;
|
|
let oldSanitizeFxaDeviceId = t.sanitizeFxaDeviceId;
|
|
t.syncIsEnabled = () => syncEnabled;
|
|
t.sanitizeFxaDeviceId = id => `So clean: ${id}`;
|
|
try {
|
|
let keep0 = Utils.makeGUID();
|
|
let keep1 = Utils.makeGUID();
|
|
let keep2 = Utils.makeGUID();
|
|
let curdev = Utils.makeGUID();
|
|
|
|
let keep1Sync = Utils.makeGUID();
|
|
let keep2Sync = Utils.makeGUID();
|
|
let curdevSync = Utils.makeGUID();
|
|
let fxaDevices = [
|
|
{
|
|
id: curdev,
|
|
isCurrentDevice: true,
|
|
lastAccessTime: Date.now() - 1000 * 60 * 60 * 24 * 1,
|
|
pushEndpointExpired: false,
|
|
type: "desktop",
|
|
name: "current device",
|
|
},
|
|
{
|
|
id: keep0,
|
|
isCurrentDevice: false,
|
|
lastAccessTime: Date.now() - 1000 * 60 * 60 * 24 * 10,
|
|
pushEndpointExpired: false,
|
|
type: "mobile",
|
|
name: "dupe",
|
|
},
|
|
// Valid 2
|
|
{
|
|
id: keep1,
|
|
isCurrentDevice: false,
|
|
lastAccessTime: Date.now() - 1000 * 60 * 60 * 24 * 1,
|
|
pushEndpointExpired: false,
|
|
type: "desktop",
|
|
name: "valid2",
|
|
},
|
|
// Valid 3
|
|
{
|
|
id: keep2,
|
|
isCurrentDevice: false,
|
|
lastAccessTime: Date.now() - 1000 * 60 * 60 * 24 * 5,
|
|
pushEndpointExpired: false,
|
|
type: "desktop",
|
|
name: "valid3",
|
|
},
|
|
];
|
|
let clientInfo = [
|
|
{
|
|
id: keep1Sync,
|
|
fxaDeviceId: keep1,
|
|
os: "Windows 30",
|
|
version: "IceCat 1 million",
|
|
},
|
|
{
|
|
id: keep2Sync,
|
|
fxaDeviceId: keep2,
|
|
os: "icecat, but an os",
|
|
verison: "twelve",
|
|
},
|
|
{
|
|
id: Utils.makeGUID(),
|
|
fxaDeviceId: null,
|
|
os: "apparently ios used to keep write these IDs as null.",
|
|
version: "Doesn't seem to anymore",
|
|
},
|
|
{
|
|
id: curdevSync,
|
|
fxaDeviceId: curdev,
|
|
os: "emacs",
|
|
version: "22",
|
|
},
|
|
{
|
|
id: Utils.makeGUID(),
|
|
fxaDeviceId: Utils.makeGUID(),
|
|
os: "not part of the fxa device set at all",
|
|
version: "foo bar baz",
|
|
},
|
|
// keep0 intententionally omitted.
|
|
];
|
|
t.getClientsEngineRecords = () => clientInfo;
|
|
let devInfo = t.updateFxaDevices(fxaDevices);
|
|
equal(devInfo.deviceID, t.sanitizeFxaDeviceId(curdev));
|
|
for (let d of devInfo.devices) {
|
|
ok(d.id.startsWith("So clean:"));
|
|
if (d.syncID) {
|
|
ok(d.syncID.startsWith("So clean:"));
|
|
}
|
|
}
|
|
equal(devInfo.devices.length, 4);
|
|
let k0 = devInfo.devices.find(d => d.id == t.sanitizeFxaDeviceId(keep0));
|
|
let k1 = devInfo.devices.find(d => d.id == t.sanitizeFxaDeviceId(keep1));
|
|
let k2 = devInfo.devices.find(d => d.id == t.sanitizeFxaDeviceId(keep2));
|
|
|
|
deepEqual(k0, {
|
|
id: t.sanitizeFxaDeviceId(keep0),
|
|
type: "mobile",
|
|
os: undefined,
|
|
version: undefined,
|
|
syncID: undefined,
|
|
});
|
|
deepEqual(k1, {
|
|
id: t.sanitizeFxaDeviceId(keep1),
|
|
type: "desktop",
|
|
os: clientInfo[0].os,
|
|
version: clientInfo[0].version,
|
|
syncID: t.sanitizeFxaDeviceId(keep1Sync),
|
|
});
|
|
deepEqual(k2, {
|
|
id: t.sanitizeFxaDeviceId(keep2),
|
|
type: "desktop",
|
|
os: clientInfo[1].os,
|
|
version: clientInfo[1].version,
|
|
syncID: t.sanitizeFxaDeviceId(keep2Sync),
|
|
});
|
|
let newCurId = Utils.makeGUID();
|
|
// Update the ID
|
|
fxaDevices[0].id = newCurId;
|
|
|
|
let keep3 = Utils.makeGUID();
|
|
fxaDevices.push({
|
|
id: keep3,
|
|
isCurrentDevice: false,
|
|
lastAccessTime: Date.now() - 1000 * 60 * 60 * 24 * 1,
|
|
pushEndpointExpired: false,
|
|
type: "desktop",
|
|
name: "valid 4",
|
|
});
|
|
devInfo = t.updateFxaDevices(fxaDevices);
|
|
|
|
let afterSubmit = [keep0, keep1, keep2, keep3, newCurId]
|
|
.map(id => t.sanitizeFxaDeviceId(id))
|
|
.sort();
|
|
deepEqual(devInfo.devices.map(d => d.id).sort(), afterSubmit);
|
|
|
|
// Reset this, as our override doesn't check for sync being enabled.
|
|
t.sanitizeFxaDeviceId = oldSanitizeFxaDeviceId;
|
|
syncEnabled = false;
|
|
fxAccounts.telemetry._setHashedUID(false);
|
|
devInfo = t.updateFxaDevices(fxaDevices);
|
|
equal(devInfo.deviceID, undefined);
|
|
equal(devInfo.devices.length, 5);
|
|
for (let d of devInfo.devices) {
|
|
equal(d.os, undefined);
|
|
equal(d.version, undefined);
|
|
equal(d.syncID, undefined);
|
|
// Type should still be present.
|
|
notEqual(d.type, undefined);
|
|
}
|
|
} finally {
|
|
t.getClientsEngineRecords = oldGetClientsEngineRecords;
|
|
t.getFxaDevices = oldGetFxaDevices;
|
|
t.syncIsEnabled = oldSyncIsEnabled;
|
|
t.sanitizeFxaDeviceId = oldSanitizeFxaDeviceId;
|
|
}
|
|
});
|
|
|
|
add_task(async function test_sanitize_fxa_device_id() {
|
|
let t = get_sync_test_telemetry();
|
|
fxAccounts.telemetry._setHashedUID(false);
|
|
sinon.stub(t, "syncIsEnabled").callsFake(() => true);
|
|
const rawDeviceId = "raw one two three";
|
|
try {
|
|
equal(t.sanitizeFxaDeviceId(rawDeviceId), null);
|
|
fxAccounts.telemetry._setHashedUID("mock uid");
|
|
const sanitizedDeviceId = t.sanitizeFxaDeviceId(rawDeviceId);
|
|
ok(sanitizedDeviceId);
|
|
notEqual(sanitizedDeviceId, rawDeviceId);
|
|
} finally {
|
|
t.syncIsEnabled.restore();
|
|
fxAccounts.telemetry._setHashedUID(false);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_no_node_type() {
|
|
let server = sync_httpd_setup({});
|
|
await configureIdentity(null, server);
|
|
|
|
await sync_and_validate_telem(ping => {
|
|
Assert.strictEqual(ping.syncNodeType, undefined);
|
|
}, true);
|
|
await promiseStopServer(server);
|
|
});
|
|
|
|
add_task(async function test_node_type() {
|
|
Service.identity.logout();
|
|
let server = sync_httpd_setup({});
|
|
await configureIdentity({ node_type: "the-node-type" }, server);
|
|
|
|
await sync_and_validate_telem(ping => {
|
|
equal(ping.syncNodeType, "the-node-type");
|
|
}, true);
|
|
await promiseStopServer(server);
|
|
});
|
|
|
|
add_task(async function test_node_type_change() {
|
|
let pingPromise = wait_for_pings(2);
|
|
|
|
Service.identity.logout();
|
|
let server = sync_httpd_setup({});
|
|
await configureIdentity({ node_type: "first-node-type" }, server);
|
|
// Default to submitting each hour - we should still submit on node change.
|
|
let telem = get_sync_test_telemetry();
|
|
telem.submissionInterval = 60 * 60 * 1000;
|
|
// reset the node type from previous test or our first sync will submit.
|
|
telem.lastSyncNodeType = null;
|
|
// do 2 syncs with the same node type.
|
|
await Service.sync();
|
|
await Service.sync();
|
|
// then another with a different node type.
|
|
Service.identity.logout();
|
|
await configureIdentity({ node_type: "second-node-type" }, server);
|
|
await Service.sync();
|
|
telem.finish();
|
|
|
|
let pings = await pingPromise;
|
|
equal(pings.length, 2);
|
|
equal(pings[0].syncs.length, 2, "2 syncs in first ping");
|
|
equal(pings[0].syncNodeType, "first-node-type");
|
|
equal(pings[1].syncs.length, 1, "1 sync in second ping");
|
|
equal(pings[1].syncNodeType, "second-node-type");
|
|
await promiseStopServer(server);
|
|
});
|
|
|
|
add_task(async function test_ids() {
|
|
let telem = get_sync_test_telemetry();
|
|
Assert.ok(!telem._shouldSubmitForDataChange());
|
|
fxAccounts.telemetry._setHashedUID("new_uid");
|
|
Assert.ok(telem._shouldSubmitForDataChange());
|
|
telem.maybeSubmitForDataChange();
|
|
// now it's been submitted the new uid is current.
|
|
Assert.ok(!telem._shouldSubmitForDataChange());
|
|
});
|
|
|
|
add_task(async function test_deletion_request_ping() {
|
|
async function assertRecordedSyncDeviceID(expected) {
|
|
// The scalar gets updated asynchronously, so wait a tick before checking.
|
|
await Promise.resolve();
|
|
const scalars =
|
|
Services.telemetry.getSnapshotForScalars("deletion-request").parent || {};
|
|
equal(scalars["deletion.request.sync_device_id"], expected);
|
|
}
|
|
|
|
const MOCK_HASHED_UID = "00112233445566778899aabbccddeeff";
|
|
const MOCK_DEVICE_ID1 = "ffeeddccbbaa99887766554433221100";
|
|
const MOCK_DEVICE_ID2 = "aabbccddeeff99887766554433221100";
|
|
|
|
// Calculated by hand using SHA256(DEVICE_ID + HASHED_UID)[:32]
|
|
const SANITIZED_DEVICE_ID1 = "dd7c845006df9baa1c6d756926519c8c";
|
|
const SANITIZED_DEVICE_ID2 = "0d06919a736fc029007e1786a091882c";
|
|
|
|
let currentDeviceID = null;
|
|
sinon.stub(fxAccounts.device, "getLocalId").callsFake(() => {
|
|
return Promise.resolve(currentDeviceID);
|
|
});
|
|
let telem = get_sync_test_telemetry();
|
|
sinon.stub(telem, "isProductionSyncUser").callsFake(() => true);
|
|
fxAccounts.telemetry._setHashedUID(false);
|
|
try {
|
|
// The scalar should start out undefined, since no user is actually logged in.
|
|
await assertRecordedSyncDeviceID(undefined);
|
|
|
|
// If we start up without knowing the hashed UID, it should stay undefined.
|
|
telem.observe(null, "weave:service:ready");
|
|
await assertRecordedSyncDeviceID(undefined);
|
|
|
|
// But now let's say we've discovered the hashed UID from the server.
|
|
fxAccounts.telemetry._setHashedUID(MOCK_HASHED_UID);
|
|
currentDeviceID = MOCK_DEVICE_ID1;
|
|
|
|
// Now when we load up, we'll record the sync device id.
|
|
telem.observe(null, "weave:service:ready");
|
|
await assertRecordedSyncDeviceID(SANITIZED_DEVICE_ID1);
|
|
|
|
// When the device-id changes we'll update it.
|
|
currentDeviceID = MOCK_DEVICE_ID2;
|
|
telem.observe(null, "fxaccounts:new_device_id");
|
|
await assertRecordedSyncDeviceID(SANITIZED_DEVICE_ID2);
|
|
|
|
// When the user signs out we'll clear it.
|
|
telem.observe(null, "fxaccounts:onlogout");
|
|
await assertRecordedSyncDeviceID("");
|
|
} finally {
|
|
fxAccounts.telemetry._setHashedUID(false);
|
|
telem.isProductionSyncUser.restore();
|
|
fxAccounts.device.getLocalId.restore();
|
|
}
|
|
});
|