Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Errors
- This test gets skipped with pattern: appname == 'thunderbird' OR verify && !debug && os == 'win'
- This test failed 69 times in the preceding 30 days. quicksearch this test
- Manifest: services/sync/tests/unit/xpcshell.toml
/* Any copyright is dedicated to the Public Domain.
const { Service } = ChromeUtils.importESModule(
);
const { WBORecord } = ChromeUtils.importESModule(
);
const { RotaryEngine } = ChromeUtils.importESModule(
);
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);
}
async function sync_engine(engine, errorRegex = undefined) {
// Clear out status so failures from previous syncs won't show in the record.
let { Status } = ChromeUtils.importESModule(
);
Status._engines = {};
Status.partial = false;
// Neuter the scheduler as it interacts badly with some of the tests - the
// engine being synced usually isn't the registered engine, so we see
// scored incremented and not removed, which schedules unexpected syncs.
let oldObserve = Service.scheduler.observe;
Service.scheduler.observe = () => {};
try {
Svc.Obs.notify("weave:service:sync:start");
let caughtError;
try {
await engine.sync();
} catch (e) {
caughtError = e;
}
if (caughtError) {
ok(
errorRegex.test(JSON.stringify(caughtError)),
"Engine sync error expected."
);
Svc.Obs.notify("weave:service:sync:error", caughtError);
} else {
ok(!errorRegex, "No engine sync error expected.");
Svc.Obs.notify("weave:service:sync:finish");
}
} finally {
Service.scheduler.observe = oldObserve;
}
}
add_setup(async function () {
// Avoid addon manager complaining about not being initialized
await Service.engineManager.unregister("addons");
await Service.engineManager.unregister("extension-storage");
do_get_profile(); // FOG requires a profile dir.
Services.fog.initializeFOG();
// We're not using `wait_for_ping` which means we aren't implicitly calling
// get_sync_test_telemetry() so often. So call it here for its side effects.
let telem = get_sync_test_telemetry();
// Unlike the telemetry tests, we actually want the Glean APIs to be called.
// So pretend we're in production.
telem.isProductionSyncUser = () => true;
});
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);
// Test that a basic batch can make it.
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
Glean.syncs.syncs.testGetValue().forEach(assert_success_sync);
},
async () => {
await Service.sync();
}
);
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 it 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]);
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
deepEqual(sync.failureReason, { name: "httperror", code: 500 });
equal(sync.engines.length, 1);
const e = sync.engines[0];
equal(e.name, "bookmarks-buffered");
deepEqual(e.failureReason, { name: "httperror", code: 500 });
},
async () => {
await sync_engine(engine, /500 Internal Server Error/);
}
);
} finally {
await store.wipe();
await cleanAndGo(engine, server);
}
});
add_task(async function test_uploading() {
// Clear out status, so failures from previous syncs won't show up in the
// telemetry ping.
let { Status } = ChromeUtils.importESModule(
);
Status._engines = {};
Status.partial = false;
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,
title: "Get Firefox!",
});
try {
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.engines.length, 1);
const e = sync.engines[0];
equal(e.name, "bookmarks-buffered");
greater(e.outgoing[0].sent, 0);
ok(!e.incoming);
},
async () => {
await sync_engine(engine);
}
);
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;
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.engines.length, 1);
const e = sync.engines[0];
equal(e.name, "bookmarks-buffered");
equal(e.outgoing.length, 1);
ok(!!e.incoming);
},
async () => {
await sync_engine(engine);
}
);
} 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;
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.engines.length, 1);
const e = sync.engines[0];
equal(e.incoming, null);
deepEqual(e.outgoing, [
{
sent: 3,
failed: 2,
failedReasons: [
{ name: "scotsman", count: 1 },
{ name: "peppercorn", count: 1 },
],
},
]);
},
async () => {
await sync_engine(engine);
}
);
await engine.setLastSync(123);
changes = await engine._tracker.getChangedIDs();
_(
`test_upload_failed: Rotary tracker contents at second sync: ${JSON.stringify(
changes
)}`
);
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.engines.length, 1);
const e = sync.engines[0];
deepEqual(e.outgoing, [
{
sent: 2,
failed: 2,
failedReasons: [
{ name: "scotsman", count: 1 },
{ name: "peppercorn", count: 1 },
],
},
]);
},
async () => {
await sync_engine(engine);
}
);
} 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;
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
ok(!sync.failureReason);
equal(sync.engines.length, 1);
const e = sync.engines[0];
equal(e.name, "rotary");
ok(!e.incoming);
ok(!e.failureReason);
deepEqual(e.outgoing, [
{
sent: 234,
failed: 2,
failedReasons: [
{ name: "record-no-23", count: 1 },
{ name: "record-no-42", count: 1 },
],
},
]);
},
async () => {
await sync_engine(engine);
}
);
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);
changes = await engine._tracker.getChangedIDs();
_(
`test_sync_partialUpload: Rotary tracker contents at second sync: ${JSON.stringify(
changes
)}`
);
// It would be nice if we had a more descriptive error for this...
const uploadFailureError = {
name: "httperror",
code: 500,
};
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
deepEqual(sync.failureReason, uploadFailureError);
equal(sync.engines.length, 1);
const e = sync.engines[0];
equal(e.name, "rotary");
deepEqual(e.incoming, {
failed: 1,
failedReasons: [
{ name: "No ciphertext: nothing to decrypt?", count: 1 },
],
});
ok(!e.outgoing);
deepEqual(e.failureReason, uploadFailureError);
},
async () => {
await sync_engine(engine, /500 Internal Server Error/);
}
);
} 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
deepEqual(
sync.engines.find(err => err.name === "steam").failureReason,
{
name: "unexpectederror",
error: String(e),
}
);
},
async () => {
await Service.sync();
}
);
} 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
deepEqual(
sync.engines.find(err => err.name === "steam").failureReason,
{
name: "unexpectederror",
error: msg,
}
);
},
async () => {
await Service.sync();
}
);
let e = { msg };
engine._errToThrow = e;
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
deepEqual(
sync.engines.find(err => err.name === "steam").failureReason,
{
name: "unexpectederror",
error: JSON.stringify(e),
}
);
},
async () => {
await Service.sync();
}
);
} 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
ok(!sync.engines.find(e => e.name === "steam"));
const eng = sync.engines.find(e => e.name === "steam-but-better");
ok(eng);
deepEqual(
eng.validation,
{
version: 1,
checked: 0,
problems: problemsToReport,
},
"Should include validation report with overridden name"
);
},
async () => {
await Service.sync();
}
);
info("Sync without validation problems");
engine.problemsToReport = null;
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
ok(!sync.engines.find(e => e.name === "steam"));
const eng = sync.engines.find(e => e.name === "steam-but-better");
ok(eng);
ok(
!eng.validation,
"Should not include validation report when there are no problems"
);
},
async () => {
await Service.sync();
}
);
} 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
const failureReason = sync.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);
},
async () => {
await Service.sync();
}
);
} finally {
await cleanAndGo(engine, server);
await Service.engineManager.unregister(engine);
}
});
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(
);
try {
const changes = await engine._tracker.getChangedIDs();
_(`test_clean_urls: Steam tracker contents: ${JSON.stringify(changes)}`);
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
const failureReason = sync.engines.find(
e => e.name === "steam"
).failureReason;
equal(failureReason.name, "unexpectederror");
equal(failureReason.error, "<URL> is not a valid URL.");
},
async () => {
await Service.sync();
}
);
// Handle other errors that include urls.
engine._errToThrow =
"Other error message that includes some:url/foo/bar/ in it.";
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
const failureReason = sync.engines.find(
e => e.name === "steam"
).failureReason;
equal(failureReason.name, "unexpectederror");
equal(
failureReason.error,
"Other error message that includes <URL> in it."
);
},
async () => {
await Service.sync();
}
);
} finally {
await cleanAndGo(engine, server);
await Service.engineManager.unregister(engine);
}
});
// 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
const failureReason = sync.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"
);
},
async () => {
await Service.sync();
}
);
} 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
)}`
);
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
equal(sync.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
sync.engines
.filter(e => telemetryEngineNames.includes(e.name))
.forEach(e => {
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);
});
},
async () => {
await Service.sync();
}
);
} 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
deepEqual(sync.status, {
service: SYNC_FAILED_PARTIAL,
sync: LOGIN_FAILED_NETWORK_ERROR,
});
const eng = sync.engines.find(e => e.name === "steam");
deepEqual(eng.failureReason, {
name: "httperror",
code: Cr.NS_ERROR_UNKNOWN_HOST,
});
},
async () => {
await Service.sync();
}
);
} 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
)}`
);
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.hashedFxaUid.testGetValue(), "f".repeat(32));
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.why, "user");
},
async () => {
await Service.sync({ 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 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);
GleanPings.sync.testBeforeNextSubmit(() => {
ok(false, "Submitted telemetry ping when we should not have.");
});
for (let i = 0; i < 5; ++i) {
await Service.sync();
}
telem.submissionInterval = -1;
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.discarded.testGetValue(), 4);
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 2);
syncs.forEach(assert_success_sync);
},
async () => {
await Service.sync(); // Sixth time's the charm.
}
);
} finally {
telem.maxPayloadCount = 500;
telem.submissionInterval = -1;
if (server) {
await promiseStopServer(server);
}
}
});
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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
equal(sync.status.service, SYNC_FAILED_PARTIAL);
ok(sync.engines.every(e => e.name !== "bogus"));
},
async () => {
await Service.sync();
}
);
} 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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
const syncs = Glean.syncs.syncs.testGetValue();
equal(syncs.length, 1);
const sync = syncs[0];
ok(sync.engines.every(e => e.name !== "bogus"));
},
async () => {
await Service.sync();
}
);
} finally {
await cleanAndGo(engine, server);
await Service.engineManager.unregister(engine);
}
});
add_task(async function test_no_node_type() {
let server = sync_httpd_setup({});
await configureIdentity(null, server);
await GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
Assert.strictEqual(Glean.syncs.syncNodeType.testGetValue(), null);
},
async () => {
await Service.sync();
}
);
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 GleanPings.sync.testSubmission(
reason => {
equal(reason, "schedule");
equal(Glean.syncs.syncNodeType.testGetValue(), "the-node-type");
},
async () => {
await Service.sync();
}
);
await promiseStopServer(server);
});
add_task(async function test_node_type_change() {
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 GleanPings.sync.testSubmission(
() => {
equal(
Glean.syncs.syncs.testGetValue().length,
2,
"2 syncs in first ping"
);
equal(Glean.syncs.syncNodeType.testGetValue(), "first-node-type");
},
async () => {
await Service.sync();
}
);
await GleanPings.sync.testSubmission(
() => {
equal(
Glean.syncs.syncs.testGetValue().length,
1,
"1 sync in second ping"
);
equal(Glean.syncs.syncNodeType.testGetValue(), "second-node-type");
},
async () => {
telem.finish();
}
);
await promiseStopServer(server);
});
add_task(async function test_deletion_request_ping() {
async function assertRecordedSyncDeviceID(expected) {
// `onAccountInitOrChange` sets the id asynchronously, so wait a tick.
await Promise.resolve();
equal(Glean.deletionRequest.syncDeviceId.testGetValue(), expected);
}
Services.fog.testResetFOG();
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(null);
// If we start up without knowing the hashed UID, it should stay undefined.
telem.observe(null, "weave:service:ready");
await assertRecordedSyncDeviceID(null);
// 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();
}
});
// TODO: Is the topic `"weave:telemetry:migration"` presently hooked up?
add_task(async function test_migration() {
let telem = get_sync_test_telemetry();
const migrationInfo = {
entries: 42,
entries_successful: 42,
extensions: 84,
extensions_successful: 83,
openFailure: false,
};
telem._addMigrationRecord("webext-storage", migrationInfo);
await GleanPings.sync.testSubmission(
() => {
const migrations = Glean.syncs.migrations.testGetValue();
equal(migrations.length, 1);
deepEqual(migrations[0], {
migration_type: "webext-storage",
entries: 42,
entriesSuccessful: 42,
extensions: 84,
extensionsSuccessful: 83,
openFailure: false,
});
},
() => {
telem.finish();
}
);
});