/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { BridgedEngine, BridgeWrapperXPCOM } = ChromeUtils.importESModule( "resource://services-sync/bridged_engine.sys.mjs" ); const { Service } = ChromeUtils.importESModule( "resource://services-sync/service.sys.mjs" ); // Wraps an `object` in a proxy so that its methods are bound to it. This // simulates how XPCOM class instances have all their methods bound. function withBoundMethods(object) { return new Proxy(object, { get(target, key) { let value = target[key]; return typeof value == "function" ? value.bind(target) : value; }, }); } add_task(async function test_interface() { class TestBridge { constructor() { this.storageVersion = 2; this.syncID = "syncID111111"; this.clear(); } clear() { this.lastSyncMillis = 0; this.wasSyncStarted = false; this.incomingEnvelopes = []; this.uploadedIDs = []; this.wasSyncFinished = false; this.wasReset = false; this.wasWiped = false; } // `mozIBridgedSyncEngine` methods. getLastSync(callback) { CommonUtils.nextTick(() => callback.handleSuccess(this.lastSyncMillis)); } setLastSync(millis, callback) { this.lastSyncMillis = millis; CommonUtils.nextTick(() => callback.handleSuccess()); } resetSyncId(callback) { CommonUtils.nextTick(() => callback.handleSuccess(this.syncID)); } ensureCurrentSyncId(newSyncId, callback) { equal(newSyncId, this.syncID, "Local and new sync IDs should match"); CommonUtils.nextTick(() => callback.handleSuccess(this.syncID)); } syncStarted(callback) { this.wasSyncStarted = true; CommonUtils.nextTick(() => callback.handleSuccess()); } storeIncoming(envelopes, callback) { this.incomingEnvelopes.push(...envelopes.map(r => JSON.parse(r))); CommonUtils.nextTick(() => callback.handleSuccess()); } apply(callback) { let outgoingEnvelopes = [ { id: "hanson", data: { plants: ["seed", "flower 💐", "rose"], canYouTell: false, }, }, { id: "sheryl-crow", data: { today: "winding 🛣", tomorrow: "winding 🛣", }, }, ].map(cleartext => JSON.stringify({ id: cleartext.id, payload: JSON.stringify(cleartext), }) ); CommonUtils.nextTick(() => callback.handleSuccess(outgoingEnvelopes)); } setUploaded(millis, ids, callback) { this.uploadedIDs.push(...ids); CommonUtils.nextTick(() => callback.handleSuccess()); } syncFinished(callback) { this.wasSyncFinished = true; CommonUtils.nextTick(() => callback.handleSuccess()); } reset(callback) { this.clear(); this.wasReset = true; CommonUtils.nextTick(() => callback.handleSuccess()); } wipe(callback) { this.clear(); this.wasWiped = true; CommonUtils.nextTick(() => callback.handleSuccess()); } } let bridge = new TestBridge(); let engine = new BridgedEngine("Nineties", Service); engine._bridge = new BridgeWrapperXPCOM(withBoundMethods(bridge)); engine.enabled = true; let server = await serverForFoo(engine); try { await SyncTestingInfrastructure(server); info("Add server records"); let foo = server.user("foo"); let collection = foo.collection("nineties"); let now = new_timestamp(); collection.insert( "backstreet", encryptPayload({ id: "backstreet", data: { say: "I want it that way", when: "never", }, }), now ); collection.insert( "tlc", encryptPayload({ id: "tlc", data: { forbidden: ["scrubs 🚫"], numberAvailable: false, }, }), now + 5 ); info("Sync the engine"); // Advance the last sync time to skip the Backstreet Boys... bridge.lastSyncMillis = 1000 * (now + 2); await sync_engine_and_validate_telem(engine, false); let metaGlobal = foo.collection("meta").wbo("global").get(); deepEqual( JSON.parse(metaGlobal.payload).engines.nineties, { version: 2, syncID: "syncID111111", }, "Should write storage version and sync ID to m/g" ); greater(bridge.lastSyncMillis, 0, "Should update last sync time"); ok( bridge.wasSyncStarted, "Should have started sync before storing incoming" ); deepEqual( bridge.incomingEnvelopes .sort((a, b) => a.id.localeCompare(b.id)) .map(({ payload, ...envelope }) => ({ cleartextAsObject: JSON.parse(payload), ...envelope, })), [ { id: "tlc", modified: now + 5, cleartextAsObject: { id: "tlc", data: { forbidden: ["scrubs 🚫"], numberAvailable: false, }, }, }, ], "Should stage incoming records from server" ); deepEqual( bridge.uploadedIDs.sort(), ["hanson", "sheryl-crow"], "Should mark new local records as uploaded" ); ok(bridge.wasSyncFinished, "Should have finished sync after uploading"); deepEqual( collection.keys().sort(), ["backstreet", "hanson", "sheryl-crow", "tlc"], "Should have all records on server" ); let expectedRecords = [ { id: "sheryl-crow", data: { today: "winding 🛣", tomorrow: "winding 🛣", }, }, { id: "hanson", data: { plants: ["seed", "flower 💐", "rose"], canYouTell: false, }, }, ]; for (let expected of expectedRecords) { let actual = collection.cleartext(expected.id); deepEqual( actual, expected, `Should upload record ${expected.id} from bridged engine` ); } await engine.resetClient(); ok(bridge.wasReset, "Should reset local storage for bridge"); await engine.wipeClient(); ok(bridge.wasWiped, "Should wipe local storage for bridge"); await engine.resetSyncID(); ok( !foo.collection("nineties"), "Should delete server collection after resetting sync ID" ); } finally { await promiseStopServer(server); await engine.finalize(); } });