/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ 'use strict'; /* global asyncStorage */ /* global BaseModule */ /* global ERROR_GET_FXA_ASSERTION */ /* global ERROR_SYNC_APP_KILLED */ /* global ERROR_UNVERIFIED_ACCOUNT */ /* global expect */ /* global FxAccountsClient */ /* global IACHandler */ /* global MockAdvancedTelemetryHelper */ /* global MocksHelper */ /* global MockNavigatormozSetMessageHandler */ /* global MockNavigatorSettings */ /* global MockService */ /* global SyncManager */ /* global SyncRecoverableErrors */ requireApp('system/js/service.js'); requireApp('system/test/unit/mock_asyncStorage.js'); requireApp('system/js/base_module.js'); requireApp('system/js/sync_state_machine.js'); requireApp('system/js/sync_manager.js'); requireApp('system/test/unit/mock_fxa_client.js'); requireApp('system/test/unit/mock_iac_handler.js'); require('/shared/test/unit/mocks/mock_lazy_loader.js'); require('/shared/js/sync/errors.js'); require('/shared/js/advanced_telemetry_helper.js'); require('/shared/test/unit/mocks/mock_service.js'); require('/shared/test/unit/mocks/mock_navigator_moz_settings.js'); require('/shared/test/unit/mocks/mock_navigator_moz_set_message_handler.js'); require('/shared/test/unit/mocks/mock_advanced_telemetry_helper.js'); var mocksForSyncManager = new MocksHelper([ 'asyncStorage', 'FxAccountsClient', 'IACHandler', 'LazyLoader', 'Service' ]).init(); suite('system/SyncManager >', () => { var realMozSetMessageHandler; var realMozSettings; var realTelemetryHelper; var systemMessageHandlerSpy; mocksForSyncManager.attachTestHelpers(); suiteSetup(() => { require('/shared/js/sync/telemetry.js'); realMozSetMessageHandler = navigator.mozSetMessageHandler; navigator.mozSetMessageHandler = MockNavigatormozSetMessageHandler; realMozSettings = navigator.mozSettings; navigator.mozSettings = MockNavigatorSettings; realTelemetryHelper = window.AdvancedTelemetryHelper; window.AdvancedTelemetryHelper = MockAdvancedTelemetryHelper; MockNavigatormozSetMessageHandler.mSetup(); MockNavigatorSettings.mSetup(); systemMessageHandlerSpy = this.sinon.spy(navigator, 'mozSetMessageHandler'); }); suiteTeardown(() => { navigator.mozSetMessageHandler = realMozSetMessageHandler; navigator.mozSettings = realMozSettings; window.AdvancedTelemetryHelper = realTelemetryHelper; MockNavigatorSettings.mTeardown(); }); suite('Initial state', () => { var syncManager; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); test('Integrity', () => { assert.ok(syncManager !== undefined); sinon.assert.calledOnce(systemMessageHandlerSpy); sinon.assert.calledWith(systemMessageHandlerSpy, 'request-sync'); }); }); suite('Initial state setting', () => { var syncManager; const syncState = 'sync.state'; const syncError = 'sync.state.error'; var nextState; var nextError; var updateStateSpy; var requestStub; suiteSetup(() => { nextState = 'enabling'; }); suiteTeardown(() => { asyncStorage.setItem(syncState, 'disabled'); asyncStorage.setItem(syncError, null); }); setup(done => { asyncStorage.setItem(syncState, nextState, () => { asyncStorage.setItem(syncError, nextError, () => { syncManager = BaseModule.instantiate('SyncManager'); updateStateSpy = this.sinon.spy(syncManager, 'updateState'); requestStub = this.sinon.stub(MockService, 'request', () => { return Promise.resolve(); }); this.sinon.stub(syncManager, 'getAccount', () => { syncManager.user = 'someone'; return Promise.resolve('someone'); }); syncManager.start().then(done); }); }); }); teardown(() => { syncManager.stop(); updateStateSpy.restore(); requestStub.restore(); }); [{ syncStateValue: 'enabling', nextSyncStateValue: 'errored', shouldDisable: true }, { syncStateValue: 'errored', nextSyncStateValue: 'errored', nextSyncUnverified: true, shouldDisable: true }, { syncStateValue: 'errored', nextSyncStateValue: 'enabled', shouldDisable: false }, { syncStateValue: 'enabled', nextSyncStateValue: 'success', shouldDisable: false }, { syncStateValue: 'success', nextSyncStateValue: 'syncing', shouldDisable: false }, { syncStateValue: 'syncing', nextSyncStateValue: 'disabling', shouldDisable: false }, { syncStateValue: 'disabling', nextSyncStateValue: 'disabled', shouldDisable: true }].forEach(config => { test('sync.state ' + config.syncStateValue, () => { if (config.shouldDisable) { this.sinon.assert.calledOnce(updateStateSpy); assert.ok(updateStateSpy.calledWith('disabled')); this.sinon.assert.notCalled(requestStub); } else { this.sinon.assert.notCalled(updateStateSpy); this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:enable')); } if (config.nextSyncUnverified) { nextError = ERROR_UNVERIFIED_ACCOUNT; } nextState = config.nextSyncStateValue; }); }); test('sync.state disabled', () => { this.sinon.assert.notCalled(updateStateSpy); this.sinon.assert.notCalled(requestStub); nextState = 'enabled'; }); }); suite('Sync management IAC based API', () => { var syncManager; var requestStub; var getPortStub; var getAccountStub; var port; var telemetryHelperSpy; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { requestStub = this.sinon.stub(MockService, 'request', () => { return Promise.resolve(); }); getPortStub = this.sinon.stub(IACHandler, 'getPort', () => { return port; }); getAccountStub = this.sinon.stub(FxAccountsClient, 'getAccount', successCb => { successCb({ email: 'user@domain.org' }); }); telemetryHelperSpy = this.sinon.spy(window, 'AdvancedTelemetryHelper'); }); teardown(() => { requestStub.restore(); getPortStub.restore(); getAccountStub.restore(); telemetryHelperSpy.restore(); }); ['enable', 'disable', 'sync'].forEach(name => { test('receive ' + name + ' IAC request', done => { window.dispatchEvent(new CustomEvent('iac-gaia-sync-management', { detail: { name: name } })); this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:' + name)); setTimeout(() => { assert.isTrue(telemetryHelperSpy.calledWithNew()); var counter = telemetryHelperSpy.getCall(0).returnValue; assert.equal(counter.name, 'telemetry_gaia_sync_user_action_' + name); assert.equal(window.AdvancedTelemetryHelper.HISTOGRAM_COUNT, counter.type); done(); }); }); }); test('getInfo request', done => { var id = Date.now(); var sentMessage; port = { postMessage: function(message) { sentMessage = message; } }; window.dispatchEvent(new CustomEvent('iac-gaia-sync-management', { detail: { name: 'getInfo', id: id } })); setTimeout(() => { this.sinon.assert.notCalled(requestStub); this.sinon.assert.calledOnce(getPortStub); assert.equal(sentMessage.id, id); assert.equal(sentMessage.state, syncManager.state); assert.equal(sentMessage.lastSync, syncManager.lastSync); assert.equal(sentMessage.user, 'user@domain.org'); done(); }); }); test('getInfo request - missing id', () => { window.dispatchEvent(new CustomEvent('iac-gaia-sync-management', { detail: { name: 'getInfo' } })); this.sinon.assert.notCalled(requestStub); this.sinon.assert.notCalled(getPortStub); }); test('receive unknown IAC request', () => { window.dispatchEvent(new CustomEvent('iac-gaia-sync-management', { detail: { name: 'whatever' } })); this.sinon.assert.notCalled(requestStub); }); }); suite('onsyncdisabled', () => { var syncManager; var realNavigatorSync; var unregisterSyncSpy; var updateStateSpy; var removeEventListenerSpy; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); realNavigatorSync = navigator.sync; navigator.sync = { unregister: function() { return Promise.resolve(); } }; unregisterSyncSpy = this.sinon.spy(navigator.sync, 'unregister'); updateStateSpy = this.sinon.spy(syncManager, 'updateState'); removeEventListenerSpy = this.sinon.spy(window, 'removeEventListener'); }); suiteTeardown(() => { syncManager.stop(); unregisterSyncSpy.restore(); updateStateSpy.restore(); removeEventListenerSpy.restore(); navigator.sync = realNavigatorSync; }); test('onsyncdisabled received', done => { window.dispatchEvent(new CustomEvent('onsyncdisabled')); setTimeout(() => { this.sinon.assert.calledOnce(unregisterSyncSpy); assert.ok(unregisterSyncSpy.calledWith('gaia::system::firefoxsync')); this.sinon.assert.calledOnce(updateStateSpy); this.sinon.assert.calledOnce(removeEventListenerSpy); assert.ok( removeEventListenerSpy.calledWith('mozFxAccountsUnsolChromeEvent') ); done(); }); }); }); suite('onsyncenabled', () => { var syncManager; var realNavigatorSync; var registerSyncSpy; var registerError; var registrationsError; var syncRegistrations = []; var updateStateSpy; var requestStub; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); realNavigatorSync = navigator.sync; }); suiteTeardown(() => { navigator.sync = realNavigatorSync; syncManager.stop(); }); setup(() => { navigator.sync = { registrations: function() { return registrationsError ? Promise.reject(registrationsError) : Promise.resolve(syncRegistrations); }, register: function() { return registerError ? Promise.reject(registerError) : Promise.resolve(); } }; registerSyncSpy = this.sinon.spy(navigator.sync, 'register'); updateStateSpy = this.sinon.spy(syncManager, 'updateState'); requestStub = this.sinon.stub(MockService, 'request', () => { return Promise.resolve(); }); syncManager.user = 'someone'; }); teardown(() => { registerSyncSpy.restore(); updateStateSpy.restore(); requestStub.restore(); }); test('onsyncenabled received - success', done => { window.dispatchEvent(new CustomEvent('onsyncenabled')); setTimeout(() => { this.sinon.assert.calledOnce(registerSyncSpy); assert.ok(registerSyncSpy.calledWith('gaia::system::firefoxsync')); this.sinon.assert.calledOnce(updateStateSpy); done(); }); }); test('onsyncenabled received - already registered', done => { teardown(() => { syncRegistrations = []; }); syncRegistrations = [{ task: 'gaia::system::firefoxsync' }]; window.dispatchEvent(new CustomEvent('onsyncenabled')); setTimeout(() => { this.sinon.assert.notCalled(registerSyncSpy); this.sinon.assert.calledOnce(updateStateSpy); done(); }); }); test('onsyncenabled received - navigator.sync.registrations error', done => { teardown(() => { registrationsError = null; }); registrationsError = 'error'; window.dispatchEvent(new CustomEvent('onsyncenabled')); setTimeout(() => { this.sinon.assert.notCalled(registerSyncSpy); this.sinon.assert.calledOnce(updateStateSpy); this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:error')); done(); }); }); test('onsyncenabled received - navigator.sync.register error', done => { teardown(() => { registerError = null; }); registerError = 'error'; window.dispatchEvent(new CustomEvent('onsyncenabled')); setTimeout(() => { this.sinon.assert.calledOnce(registerSyncSpy); this.sinon.assert.calledOnce(updateStateSpy); this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:error')); done(); }); }); test('onsyncenabled received - no user should disable sync', done => { syncManager.user = null; window.dispatchEvent(new CustomEvent('onsyncenabled')); setTimeout(() => { this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:disable')); done(); }); }); }); suite('onsyncenabling', () => { var syncManager; var updateStateStub; var requestStub; var getAssertionStub; var getAssertionError; var getKeysStub; var getKeysError; var trySyncStub; var trySyncError; var getAccountStub; var addEventListenerSpy; var saveDefaultSettingsStub; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { updateStateStub = this.sinon.stub(syncManager, 'updateState', () => { syncManager.updateStateDeferred = Promise.resolve(); }); addEventListenerSpy = this.sinon.spy(window, 'addEventListener'); requestStub = this.sinon.stub(MockService, 'request', () => { return Promise.resolve(); }); getAssertionStub = this.sinon.stub(syncManager, 'getAssertion', () => { return getAssertionError ? Promise.reject(getAssertionError) : Promise.resolve(); }); getKeysStub = this.sinon.stub(syncManager, 'getKeys', () => { return getKeysError ? Promise.reject(getKeysError) : Promise.resolve(); }); trySyncStub = this.sinon.stub(syncManager, 'trySync', () => { return trySyncError ? Promise.reject(trySyncError) : Promise.resolve(); }); getAccountStub = this.sinon.stub(syncManager, 'getAccount', () => { return Promise.resolve(); }); saveDefaultSettingsStub = this.sinon.stub(syncManager, 'saveDefaultSettings', () => { return Promise.resolve(); }); }); teardown(() => { addEventListenerSpy.restore(); updateStateStub.restore(); requestStub.restore(); getAssertionStub.restore(); getKeysStub.restore(); trySyncStub.restore(); getAccountStub.restore(); saveDefaultSettingsStub.restore(); }); test('onsyncenabling received - success', done => { window.dispatchEvent(new CustomEvent('onsyncenabling')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.calledOnce(getKeysStub); this.sinon.assert.calledOnce(trySyncStub); this.sinon.assert.calledOnce(getAccountStub); this.sinon.assert.calledOnce(addEventListenerSpy); this.sinon.assert.calledWith(addEventListenerSpy, 'mozFxAccountsUnsolChromeEvent'); this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:success')); done(); }); }); test('onsyncenabling received - getAssertion error', done => { teardown(() => { getAssertionError = null; }); getAssertionError = 'UNVERIFIED_ACCOUNT'; window.dispatchEvent(new CustomEvent('onsyncenabling')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.notCalled(getKeysStub); this.sinon.assert.notCalled(trySyncStub); this.sinon.assert.calledOnce(getAccountStub); this.sinon.assert.calledOnce(addEventListenerSpy); this.sinon.assert.calledWith(addEventListenerSpy, 'mozFxAccountsUnsolChromeEvent'); this.sinon.assert.calledOnce(requestStub); assert.equal(requestStub.getCall(0).args[0], 'SyncStateMachine:error'); assert.equal(requestStub.getCall(0).args[1], ERROR_UNVERIFIED_ACCOUNT); done(); }); }); test('onsyncenabling received - getKeys error', done => { teardown(() => { getKeysError = null; }); getKeysError = Date.now(); window.dispatchEvent(new CustomEvent('onsyncenabling')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.calledOnce(getKeysStub); this.sinon.assert.notCalled(trySyncStub); this.sinon.assert.notCalled(getAccountStub); this.sinon.assert.calledOnce(addEventListenerSpy); this.sinon.assert.calledWith(addEventListenerSpy, 'mozFxAccountsUnsolChromeEvent'); this.sinon.assert.calledOnce(requestStub); assert.equal(requestStub.getCall(0).args[0], 'SyncStateMachine:error'); assert.equal(requestStub.getCall(0).args[1], getKeysError); done(); }); }); test('onsyncenabling received - trySync error', done => { teardown(() => { trySyncError = null; }); trySyncError = Date.now(); window.dispatchEvent(new CustomEvent('onsyncenabling')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.calledOnce(getKeysStub); this.sinon.assert.calledOnce(trySyncStub); this.sinon.assert.calledOnce(getAccountStub); this.sinon.assert.calledOnce(addEventListenerSpy); this.sinon.assert.calledWith(addEventListenerSpy, 'mozFxAccountsUnsolChromeEvent'); this.sinon.assert.calledOnce(requestStub); assert.equal(requestStub.getCall(0).args[0], 'SyncStateMachine:error'); assert.equal(requestStub.getCall(0).args[1], trySyncError); done(); }); }); }); suite('onsyncerrored', () => { var syncManager; var updateStateSpy; var requestStub; var logoutSpy; const ERROR_SYNC_APP_KILLED = 'fxsync-error-app-killed'; const ERROR_SYNC_APP_SYNC_IN_PROGRESS = 'fxsync-error-app-fxsync-in-progress'; const ERROR_SYNC_APP_GENERIC = 'fxsync-error-app-generic'; const ERROR_SYNC_APP_TRY_LATER = 'fxsync-error-app-try-later'; const ERROR_UNVERIFIED_ACCOUNT = 'fxsync-error-unverified-account'; const ERROR_DIALOG_CLOSED_BY_USER = 'fxsync-error-dialog-closed-by-user'; const ERROR_GET_FXA_ASSERTION = 'fxsync-error-get-fxa-assertion'; const ERROR_INVALID_SYNC_ACCOUNT = 'fxsync-error-invalid-account'; const ERROR_OFFLINE = 'fxsync-error-offline'; const ERROR_REQUEST_SYNC_REGISTRATION = 'fxsync-error-request-fxsync-registration'; const ERROR_SYNC_INVALID_REQUEST_OPTIONS = 'fxsync-error-invalid-request-options'; const ERROR_SYNC_REQUEST = 'fxsync-error-request-failed'; const ERROR_NO_KEY_FETCH_TOKEN = 'fxsync-error-no-key-fetch-token'; const ERROR_UNKNOWN = 'fxsync-error-unknown'; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { updateStateSpy = this.sinon.spy(syncManager, 'updateState'); requestStub = this.sinon.stub(MockService, 'request', () => { return Promise.resolve(); }); logoutSpy = this.sinon.spy(FxAccountsClient, 'logout'); }); teardown(() => { updateStateSpy.restore(); requestStub.restore(); logoutSpy.restore(); }); const errors = [ ERROR_SYNC_APP_KILLED, ERROR_SYNC_APP_SYNC_IN_PROGRESS, ERROR_SYNC_APP_GENERIC, ERROR_SYNC_APP_TRY_LATER, ERROR_UNVERIFIED_ACCOUNT, ERROR_DIALOG_CLOSED_BY_USER, ERROR_GET_FXA_ASSERTION, ERROR_INVALID_SYNC_ACCOUNT, ERROR_OFFLINE, ERROR_REQUEST_SYNC_REGISTRATION, ERROR_SYNC_INVALID_REQUEST_OPTIONS, ERROR_SYNC_REQUEST, ERROR_NO_KEY_FETCH_TOKEN, ERROR_UNKNOWN ]; errors.forEach(error => { test(`onerrored received while NOT syncing - ${error}`, done => { window.dispatchEvent(new CustomEvent('onsyncerrored', { detail: { args: [error] } })); setTimeout(() => { this.sinon.assert.calledOnce(updateStateSpy); assert.equal(syncManager.error, error); if (error == ERROR_UNVERIFIED_ACCOUNT) { this.sinon.assert.notCalled(requestStub); } else if (error == ERROR_INVALID_SYNC_ACCOUNT) { assert.ok(requestStub.calledWith('SyncStateMachine:enable')); } else if (error == ERROR_NO_KEY_FETCH_TOKEN) { assert.ok(logoutSpy.calledOnce); } else { assert.ok(requestStub.calledWith('SyncStateMachine:disable')); } done(); }); }); }); errors.forEach(error => { test(`onerrored received while syncing - ${error}`, done => { window.dispatchEvent(new CustomEvent('onsyncerrored', { detail: { args: [error], from: 'syncing' } })); setTimeout(() => { this.sinon.assert.calledOnce(updateStateSpy); assert.equal(syncManager.error, error); if (error == ERROR_UNVERIFIED_ACCOUNT) { this.sinon.assert.notCalled(requestStub); } else if (SyncRecoverableErrors.indexOf(error) > -1 || error == ERROR_INVALID_SYNC_ACCOUNT) { assert.ok(requestStub.calledWith('SyncStateMachine:enable')); } else if (error == ERROR_NO_KEY_FETCH_TOKEN) { assert.ok(logoutSpy.calledOnce); } else { assert.ok(requestStub.calledWith('SyncStateMachine:disable')); } done(); }); }); }); }); suite('onsyncsyncing', () => { var syncManager; var updateStateStub; var unregisterSyncStub; var requestStub; var getAssertionStub; var getAssertionError; var getKeysStub; var getKeysError; var realMozApps; var realUuid; var port; var portStub; var result; var assertion = 'assertion'; var keys = 'keys'; var id; suiteSetup(() => { realMozApps = navigator.mozApps; realUuid = window.uuid; port = { postMessage: function() {}, set onmessage(callback) { setTimeout(() => { callback({ data: result }); }); } }; var app = { connect: function() { return Promise.resolve([port]); } }; var onsuccess = { set onsuccess(callback) { setTimeout(() => { callback({ target: { result: app } }); }); } }; navigator.mozApps = { getSelf: function() { return onsuccess; } }; window.uuid = () => { return id; }; syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); navigator.mozApps = realMozApps; window.uuid = realUuid; }); setup(() => { updateStateStub = this.sinon.stub(syncManager, 'updateState'); unregisterSyncStub = this.sinon.stub(syncManager, 'unregisterSyncRequest'); requestStub = this.sinon.stub(MockService, 'request', () => { return Promise.resolve(); }); getAssertionStub = this.sinon.stub(syncManager, 'getAssertion', () => { return getAssertionError ? Promise.reject(getAssertionError) : Promise.resolve(assertion); }); getKeysStub = this.sinon.stub(syncManager, 'getKeys', () => { return getKeysError ? Promise.reject(getKeysError) : Promise.resolve(keys); }); portStub = this.sinon.stub(port, 'postMessage'); syncManager.state = 'syncing'; }); teardown(() => { updateStateStub.restore(); unregisterSyncStub.restore(); requestStub.restore(); getKeysStub.restore(); portStub.restore(); getAssertionStub.restore(); }); test('onsyncsyncing - success', done => { id = Date.now(); result = { id: id }; var previousLastSync = syncManager.lastSync; syncManager._settings['sync.collections.history.enabled'] = true; syncManager._settings['sync.collections.history.readonly'] = true; window.dispatchEvent(new CustomEvent('onsyncsyncing')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(unregisterSyncStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.calledOnce(getKeysStub); setTimeout(() => { this.sinon.assert.calledOnce(portStub); assert.equal(portStub.getCall(0).args[0].assertion, assertion); assert.equal(portStub.getCall(0).args[0].keys, keys); assert.ok(portStub.getCall(0).args[0].collections.history); assert.ok(portStub.getCall(0).args[0].collections.history.readonly); setTimeout(() => { assert.notEqual(syncManager.lastSync, previousLastSync); this.sinon.assert.calledOnce(requestStub); assert.ok(requestStub.calledWith('SyncStateMachine:success')); done(); }); }); }); }); test('onsyncsyncing - no collections selected', done => { syncManager._settings['sync.collections.history.enabled'] = false; syncManager._settings['sync.collections.passwords.enabled'] = false; var previousLastSync = syncManager.lastSync; window.dispatchEvent(new CustomEvent('onsyncsyncing')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(unregisterSyncStub); this.sinon.assert.notCalled(getAssertionStub); this.sinon.assert.notCalled(getKeysStub); this.sinon.assert.notCalled(portStub); this.sinon.assert.calledOnce(requestStub); assert.equal(syncManager.lastSync, previousLastSync); assert.ok(requestStub.calledWith('SyncStateMachine:success')); done(); }); }); test('onsyncsyncing - getAssertion error', done => { teardown(() => { getAssertionError = null; }); getAssertionError = 'error'; syncManager._settings['sync.collections.history.enabled'] = true; var previousLastSync = syncManager.lastSync; window.dispatchEvent(new CustomEvent('onsyncsyncing')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(unregisterSyncStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.notCalled(getKeysStub); setTimeout(() => { this.sinon.assert.notCalled(portStub); this.sinon.assert.calledOnce(requestStub); assert.equal(syncManager.lastSync, previousLastSync); assert.ok(requestStub.calledWith('SyncStateMachine:error')); done(); }); }); }); test('onsyncsyncing - getKeys error', done => { teardown(() => { getKeysError = null; }); getKeysError = 'error'; syncManager._settings['sync.collections.history.enabled'] = true; var previousLastSync = syncManager.lastSync; window.dispatchEvent(new CustomEvent('onsyncsyncing')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(unregisterSyncStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.calledOnce(getKeysStub); setTimeout(() => { this.sinon.assert.notCalled(portStub); this.sinon.assert.calledOnce(requestStub); assert.equal(syncManager.lastSync, previousLastSync); assert.ok(requestStub.calledWith('SyncStateMachine:error')); done(); }); }); }); test('onsyncsyncing - sync app error', done => { id = Date.now(); result = { id: id, error: 'error' }; syncManager._settings['sync.collections.passwords.enabled'] = true; syncManager._settings['sync.collections.passwords.readonly'] = false; var previousLastSync = syncManager.lastSync; window.dispatchEvent(new CustomEvent('onsyncsyncing')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(unregisterSyncStub); this.sinon.assert.calledOnce(getAssertionStub); this.sinon.assert.calledOnce(getKeysStub); setTimeout(() => { this.sinon.assert.calledOnce(portStub); assert.equal(portStub.getCall(0).args[0].assertion, assertion); assert.equal(portStub.getCall(0).args[0].keys, keys); assert.ok(portStub.getCall(0).args[0].collections.passwords); assert.ok( !portStub.getCall(0).args[0].collections.passwords.readonly ); setTimeout(() => { this.sinon.assert.calledOnce(requestStub); assert.equal(syncManager.lastSync, previousLastSync); assert.ok(requestStub.calledWith('SyncStateMachine:error')); done(); }); }); }); }); test('onsyncing - offline', done => { Object.defineProperty(navigator, 'onLine', { configurable: true, get: () => { return false; }, set: () => {} }); var previousLastSync = syncManager.lastSync; window.dispatchEvent(new CustomEvent('onsyncsyncing')); setTimeout(() => { this.sinon.assert.calledOnce(updateStateStub); this.sinon.assert.calledOnce(unregisterSyncStub); this.sinon.assert.notCalled(getAssertionStub); this.sinon.assert.notCalled(getKeysStub); this.sinon.assert.notCalled(portStub); this.sinon.assert.calledOnce(requestStub); assert.equal(syncManager.lastSync, previousLastSync); assert.ok(requestStub.calledWith('SyncStateMachine:success')); done(); }); }); }); suite('Firefox Accounts - onlogout', () => { var syncManager; var requestSpy; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); requestSpy = this.sinon.spy(MockService, 'request'); }); suiteTeardown(() => { syncManager.stop(); requestSpy.restore(); }); test('FxA logout should disable Sync', done => { requestSpy.reset(); window.dispatchEvent(new CustomEvent('mozFxAccountsUnsolChromeEvent', { detail: { eventName: 'onlogout' } })); setTimeout(() => { assert.ok(requestSpy.calledWith('SyncStateMachine:disable')); done(); }); }); }); suite('Firefox Accounts - onverified', () => { var syncManager; var requestSpy; var getAccountStub; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); requestSpy = this.sinon.spy(MockService, 'request'); getAccountStub = this.sinon.stub(FxAccountsClient, 'getAccount', successCb => { successCb({ email: 'user@domain.org' }); }); }); suiteTeardown(() => { syncManager.stop(); requestSpy.restore(); }); test('FxA onverified should enable Sync', done => { requestSpy.reset(); syncManager.state = 'errored'; syncManager.error = ERROR_UNVERIFIED_ACCOUNT; window.dispatchEvent(new CustomEvent('mozFxAccountsUnsolChromeEvent', { detail: { eventName: 'onverified' } })); setTimeout(() => { assert.ok(requestSpy.calledWith('SyncStateMachine:enable')); done(); }); }); test('FxA onverified and not ERROR_UNVERIFIED_ACCOUNT error ' + 'should not enable Sync', done => { requestSpy.reset(); syncManager.state = 'errored'; syncManager.error = ERROR_GET_FXA_ASSERTION; window.dispatchEvent(new CustomEvent('mozFxAccountsUnsolChromeEvent', { detail: { eventName: 'onverified' } })); setTimeout(() => { this.sinon.assert.notCalled(requestSpy); done(); }); }); }); suite('FxA getAssertion', () => { var syncManager; var getAssertionStub; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); teardown(() => { syncManager.stop(); }); setup(() => { getAssertionStub = this.sinon.stub(FxAccountsClient, 'getAssertion', (options, successCb, errorCb) => { successCb('assertion'); }); }); teardown(() => { getAssertionStub.restore(); }); test('No audience', done => { syncManager._settings['sync.fxa.audience'] = null; syncManager.getAssertion().then(result => { assert.equal(result, 'assertion'); assert.ok(getAssertionStub.calledOnce); assert.deepEqual(getAssertionStub.args[0][0], { audience: null }); done(); }); }); test('Audience set', done => { syncManager._settings['sync.fxa.audience'] = 'audience'; syncManager.getAssertion().then(result => { assert.equal(result, 'assertion'); assert.ok(getAssertionStub.calledOnce); assert.deepEqual(getAssertionStub.args[0][0], { audience: 'audience' }); done(); }); }); }); suite('updateState', () => { var syncManager; var queryStub; var queryStubResult = 'qsState'; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { queryStub = this.sinon.stub(MockService, 'query', () => { return queryStubResult; }); }); teardown(() => { queryStub.restore(); }); test('updateState with params should not query state machine', () => { var state = 'state'; syncManager.updateState(state); assert.equal(syncManager.state, state); this.sinon.assert.notCalled(queryStub); }); test('updateState w/o params should query state machine', () => { syncManager.updateState(); assert.equal(syncManager.state, queryStubResult); this.sinon.assert.calledOnce(queryStub); assert.ok(queryStub.calledWith('SyncStateMachine.state')); }); }); suite('Synchronizer killed while syncing', () => { var syncManager; var realMozApps; var realUuid; var port; var _onclose; var requestSpy; var deferred = {}; deferred.promise = new Promise(resolve => { deferred.resolve = resolve; }); suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); requestSpy = this.sinon.spy(MockService, 'request'); realMozApps = navigator.mozApps; realUuid = window.uuid; port = { set onclose(cb) { _onclose = cb; }, postMessage() { setTimeout(() => { _onclose(); deferred.resolve(); }); } }; var ports = [port]; var app = { connect() { return Promise.resolve(ports); } }; var onsuccess = { set onsuccess(callback) { setTimeout(() => { callback({ target: { result: app } }); }); } }; navigator.mozApps = { getSelf: function() { return onsuccess; } }; window.uuid = () => { return Date.now(); }; }); suiteTeardown(() => { syncManager.stop(); navigator.mozApps = realMozApps; window.uuid = realUuid; requestSpy.restore(); }); test('Should throw error if port closes while syncing', done => { syncManager.state = 'syncing'; deferred.promise.then(() => { assert.equal(requestSpy.getCall(0).args[0], 'SyncStateMachine:error'); assert.equal(requestSpy.getCall(0).args[1], ERROR_SYNC_APP_KILLED); done(); }); syncManager.doSync(); }); }); suite('appterminated', () => { var syncManager; var isSyncAppStub; var isSyncApp; function appterminated() { window.dispatchEvent(new CustomEvent('appterminated', { detail: { origin: 'whatever' } })); } suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { isSyncAppStub = this.sinon.stub(syncManager, 'isSyncApp', () => { return isSyncApp ? Promise.resolve() : Promise.reject(); }); }); teardown(() => { isSyncAppStub.restore(); }); test('No port', done => { syncManager._port = undefined; appterminated(); setTimeout(() => { assert.ok(isSyncAppStub.notCalled); done(); }); }); test('Is Sync app', done => { isSyncApp = true; syncManager._port = {}; appterminated(); setTimeout(() => { assert.isNull(syncManager._port); done(); }); }); test('Is not Sync app', done => { isSyncApp = false; syncManager._port = {}; appterminated(); setTimeout(() => { assert.ok(syncManager._port); done(); }); }); }); suite('isSyncApp', () => { var syncManager; var realMozApps; var connections = []; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); realMozApps = navigator.mozApps; var app = { getConnections: function() { return Promise.resolve(connections); } }; var onsuccess = { set onsuccess(callback) { setTimeout(() => { callback({ target: { result: app } }); }); } }; navigator.mozApps = { getSelf: function() { return onsuccess; } }; }); suiteTeardown(() => { syncManager.stop(); navigator.mozApps = realMozApps; }); setup(() => { connections = []; }); [{ test: 'No origin', origin: undefined }, { test: 'No connections', origin: 'origin' }, { test: 'No matching connection', origin: 'origin', connections: [{ keyword: 'keyword', origin: 'anotherorigin' }] }].forEach(config => { connections = config.connections; test(config.test, done => { expect(syncManager.isSyncApp(config.origin)) .to.be.rejectedWith(undefined).and.notify(done); }); }); test('Matching connection', done => { connections = [{ keyword: 'gaia::sync::request', origin: 'origin', subscriber: 'origin' }]; syncManager.isSyncApp('origin').then(() => { assert.ok(false, 'Unexpected resolution'); done(); }).catch(() => { assert.ok(true, 'Expected rejection'); done(); }); }); }); suite('cleanup', () => { var syncManager; var unregisterSyncStub; var removeEventListenerStub; var cancelSyncStub; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { unregisterSyncStub = this.sinon.stub(syncManager, 'unregisterSyncRequest'); removeEventListenerStub = this.sinon.stub(window, 'removeEventListener'); cancelSyncStub = this.sinon.stub(syncManager, 'cancelSync'); }); teardown(() => { unregisterSyncStub.restore(); removeEventListenerStub.restore(); cancelSyncStub.restore(); }); test('Cleanup', () => { syncManager.cleanup(); assert.ok(unregisterSyncStub.calledOnce); assert.ok(removeEventListenerStub.calledOnce); assert.ok(cancelSyncStub.notCalled); }); test('Cleanup while syncing', () => { syncManager.state = 'syncing'; syncManager.cleanup(); assert.ok(unregisterSyncStub.calledOnce); assert.ok(removeEventListenerStub.calledOnce); assert.ok(cancelSyncStub.calledOnce); }); }); suite('updateStatePreference', () => { var syncManager; var updateStatePreferenceSpy; var initialSettingValue; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); initialSettingValue = MockNavigatorSettings.mSettings['services.sync.enabled']; }); suiteTeardown(() => { syncManager.stop(); MockNavigatorSettings.mSettings['services.sync.enabled'] = initialSettingValue; }); setup(() => { updateStatePreferenceSpy = this.sinon.spy(syncManager, 'updateStatePreference'); }); teardown(() => { updateStatePreferenceSpy.restore(); }); ['enabled', 'enabling'].forEach(state => { test(state + ' should update setting', done => { MockNavigatorSettings.mSettings['services.sync.enabled'] = false; syncManager.state = state; assert.ok(updateStatePreferenceSpy.calledOnce); assert.ok(MockNavigatorSettings.mSettings['services.sync.enabled']); syncManager.updateStateDeferred.then(done); window.dispatchEvent(new CustomEvent('mozPrefChromeEvent', { detail: { prefName: 'services.sync.enabled', value: true } })); }); }); test('disabled should update setting', done => { MockNavigatorSettings.mSettings['services.sync.enabled'] = true; syncManager.state = 'disabled'; assert.ok(updateStatePreferenceSpy.calledOnce); assert.ok(!MockNavigatorSettings.mSettings['services.sync.enabled']); syncManager.updateStateDeferred.then(done); window.dispatchEvent(new CustomEvent('mozPrefChromeEvent', { detail: { prefName: 'services.sync.enabled', value: false } })); }); ['errored', 'success', 'syncing'].forEach(state => { test(state + ' should not update setting', () => { var initialValue = MockNavigatorSettings.mSettings['services.sync.enabled']; syncManager.state = state; assert.ok(updateStatePreferenceSpy.notCalled); assert.equal(MockNavigatorSettings.mSettings['services.sync.enabled'], initialValue); }); }); }); suite('trySync', () => { var syncManager; var iacRequestStub; var iacRequestError; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { iacRequestStub = this.sinon.stub(syncManager, 'iacRequest', () => { return Promise.resolve(iacRequestError ? { error: { message: iacRequestError } } : undefined); }); }); teardown(() => { iacRequestStub.restore(); }); test('trySync success', done => { syncManager.trySync().then(done).catch(error => { assert.ok(false, 'Unexpected error ' + error); done(); }); }); test('trySync error', done => { iacRequestError = Date.now(); syncManager.trySync().then(() => { assert.ok(false, 'Unexpected success'); done(); }).catch(error => { assert.equal(error, iacRequestError); done(); }); }); }); suite('Sync after enabled', () => { var syncManager; var registerSyncStub; var requestStub; suiteSetup(() => { syncManager = BaseModule.instantiate('SyncManager'); syncManager.start(); }); suiteTeardown(() => { syncManager.stop(); }); setup(() => { registerSyncStub = this.sinon.stub(syncManager, 'registerSyncRequest', () => { return Promise.resolve(); }); requestStub = this.sinon.stub(MockService, 'request'); }); teardown(() => { registerSyncStub.restore(); requestStub.restore(); }); test('should sync after enabled if coming from enabling', () => { syncManager._handle_onsyncenabled({ detail: { from: 'enabling' } }); setTimeout(() => { assert.ok(requestStub.calledOnce); }); }); }); suite('User settings', () => { var syncManager; var currentDeferred; function getDeferred() { var deferred = {}; deferred.promise = new Promise(resolve => { deferred.resolve = resolve; }); return deferred; } var _defaults = {}; var _user = {}; var promiseResolve = () => { return Promise.resolve(); }; suiteSetup(() => { syncManager = new SyncManager(); syncManager.start(); this.sinon.stub(syncManager, 'updateState', () => { syncManager.updateStateDeferred = Promise.resolve(); }); this.sinon.stub(syncManager, 'getAssertion', promiseResolve); this.sinon.stub(syncManager, 'getKeys', promiseResolve); this.sinon.stub(syncManager, 'trySync', promiseResolve); this.sinon.stub(syncManager, 'getAccount', promiseResolve); this.sinon.stub(MockService, 'request', (what) => { if (what !== 'SyncStateMachine:success') { return; } currentDeferred.resolve(); }); SyncManager.SETTINGS.forEach(setting => { _defaults[setting] = Date.now(); MockNavigatorSettings.mSettings[setting] = _defaults[setting]; syncManager._settings[setting] = _defaults[setting]; }); }); suiteTeardown(() => { syncManager.stop(); }); // Because we are using MockNavigatorMozSettings, the settings observers // set by BaseModule are not fired, so we need to do it manually. function copySettings() { SyncManager.SETTINGS.forEach(setting => { syncManager._settings[setting] = MockNavigatorSettings.mSettings[setting]; }); } test('should save and restore user settings on login/logout', done => { var user = 'user@mozilla.org'; syncManager.user = user; syncManager._handle_onsyncenabling(); currentDeferred = getDeferred(); currentDeferred.promise.then(() => { // Enabling should save default settings into asyncStorage. return Promise.all(SyncManager.SETTINGS.map(setting => { return new Promise(resolve => { asyncStorage.getItem(setting, value => { expect(value).to.equal(_defaults[setting]); resolve(); }); }); })); }).then(() => { // Local changes to settings. return Promise.all(SyncManager.SETTINGS.map(setting => { return new Promise(resolve => { _user[setting] = Date.now(); MockNavigatorSettings.mSettings[setting] = _user[setting]; syncManager._settings[setting] = _user[setting]; resolve(); }); })); }).then(() => { syncManager.user = user; syncManager._handle_onsyncdisabling(); copySettings(); currentDeferred = getDeferred(); return currentDeferred.promise; }).then(() => { // Disabling should restore default setting from asyncStorage. // And save user settings. return Promise.all(SyncManager.SETTINGS.map(setting => { return new Promise(resolve => { asyncStorage.getItem(setting, value => { expect(value).to.equal(null); expect(MockNavigatorSettings.mSettings[setting]) .to.equal(_defaults[setting]); asyncStorage.getItem(user + '.' + setting, value => { console.log(`value ${value} == ${_user[setting]}`); expect(value).to.equal(_user[setting]); resolve(); }); }); }); })); }).then(() => { // Login with the same user should restore user settings. syncManager.user = user; syncManager._handle_onsyncenabling(); copySettings(); currentDeferred = getDeferred(); return currentDeferred.promise; }).then(() => { return Promise.all(SyncManager.SETTINGS.map(setting => { return new Promise(resolve => { expect(MockNavigatorSettings.mSettings[setting]) .to.equal(_user[setting]); resolve(); }); })); }).then(() => { syncManager.user = user; syncManager._handle_onsyncdisabling(); currentDeferred = getDeferred(); return currentDeferred.promise; }).then(() => { // Login with a new user should restore default settings. syncManager.user = 'newuser@mozilla.org'; syncManager._handle_onsyncenabling(); copySettings(); currentDeferred = getDeferred(); return currentDeferred.promise; }).then(() => { return Promise.all(SyncManager.SETTINGS.map(setting => { return new Promise(resolve => { expect(MockNavigatorSettings.mSettings[setting]) .to.equal(_defaults[setting]); resolve(); }); })); }).then(() => { done(); }).catch(error => { assert.ok(false, error); }); }); }); });