diff --git a/src/js/app-config.js b/src/js/app-config.js index 169d00c..7f41f90 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -43,7 +43,8 @@ define(function(require) { checkOutboxInterval: 5000, iconPath: '/img/icon.png', verificationUrl: '/verify/', - verificationUuidLength: 36 + verificationUuidLength: 36, + dbVersion: 1 }; /** diff --git a/src/js/util/update/update-handler.js b/src/js/util/update/update-handler.js new file mode 100644 index 0000000..f8c6388 --- /dev/null +++ b/src/js/util/update/update-handler.js @@ -0,0 +1,90 @@ +define(function(require) { + 'use strict'; + + var cfg = require('js/app-config').config, + updateV1 = require('js/util/update/update-v1'); + + /** + * Handles database migration + */ + var UpdateHandler = function(appConfigStorage, userStorage) { + this._appConfigStorage = appConfigStorage; + this._userStorage = userStorage; + this._updateScripts = [updateV1]; + }; + + /** + * Executes all the necessary updates + * @param {Function} callback(error) Invoked when all the database updates were executed, or if an error occurred + */ + UpdateHandler.prototype.update = function(callback) { + var self = this, + currentVersion = 0, + targetVersion = cfg.dbVersion, + versionDbType = 'dbVersion'; + + self._appConfigStorage.listItems(versionDbType, 0, null, function(err, items) { + if (err) { + callback(err); + return; + } + + // parse the database version number + if (items && items.length > 0) { + currentVersion = parseInt(items[0], 10); + } + + self._applyUpdate({ + currentVersion: currentVersion, + targetVersion: targetVersion + }, callback); + }); + }; + + /** + * Schedules necessary updates and executes thom in order + */ + UpdateHandler.prototype._applyUpdate = function(options, callback) { + var self = this, + storage, + queue = []; + + if (options.currentVersion >= options.targetVersion) { + // the current database version is up to date + callback(); + return; + } + + storage = { + appConfigStorage: self._appConfigStorage, + userStorage: self._userStorage + }; + + // add all the necessary database updates to the queue + for (var i = options.currentVersion; i < options.targetVersion; i++) { + queue.push(self._updateScripts[i]); + } + + // takes the next update from the queue and executes it + function executeNextUpdate(err) { + if (err) { + callback(err); + return; + } + + if (queue.length < 1) { + // we're done + callback(); + return; + } + + // process next update + var script = queue.shift(); + script(storage, executeNextUpdate); + } + + executeNextUpdate(); + }; + + return UpdateHandler; +}); \ No newline at end of file diff --git a/src/js/util/update/update-v1.js b/src/js/util/update/update-v1.js new file mode 100644 index 0000000..87e4f41 --- /dev/null +++ b/src/js/util/update/update-v1.js @@ -0,0 +1,29 @@ +define(function() { + 'use strict'; + + /** + * Update handler for transition databasae version 0 -> 1 + * + * In database version 1, the stored email objects have to be purged, otherwise + * every non-prefixed mail in the IMAP folders would be nuked due to the implementation + * of the delta sync. + */ + function updateV1(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 1; + + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } + + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); + } + + return updateV1; +}); \ No newline at end of file diff --git a/test/new-unit/main.js b/test/new-unit/main.js index 754531a..e8776f8 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -49,7 +49,8 @@ function startTests() { 'test/new-unit/mail-list-ctrl-test', 'test/new-unit/write-ctrl-test', 'test/new-unit/outbox-bo-test', - 'test/new-unit/invitation-dao-test' + 'test/new-unit/invitation-dao-test', + 'test/new-unit/update-handler-test' ], function() { //Tests loaded, run tests mocha.run(); diff --git a/test/new-unit/update-handler-test.js b/test/new-unit/update-handler-test.js new file mode 100644 index 0000000..7894694 --- /dev/null +++ b/test/new-unit/update-handler-test.js @@ -0,0 +1,166 @@ +define(function(require) { + 'use strict'; + + var DeviceStorageDAO = require('js/dao/devicestorage-dao'), + cfg = require('js/app-config').config, + UpdateHandler = require('js/util/update/update-handler'), + expect = chai.expect; + + chai.Assertion.includeStack = true; + + describe('UpdateHandler', function() { + var updateHandler, appConfigStorageStub, userStorageStub, origDbVersion; + + beforeEach(function() { + origDbVersion = cfg.dbVersion; + appConfigStorageStub = sinon.createStubInstance(DeviceStorageDAO); + userStorageStub = sinon.createStubInstance(DeviceStorageDAO); + updateHandler = new UpdateHandler(appConfigStorageStub, userStorageStub); + }); + + afterEach(function() { + cfg.dbVersion = origDbVersion; + }); + + describe('#constructor', function() { + it('should create instance', function() { + expect(updateHandler).to.exist; + expect(updateHandler._appConfigStorage).to.equal(appConfigStorageStub); + expect(updateHandler._userStorage).to.equal(userStorageStub); + + // the update handler must contain as many db update sripts as there are database versions + expect(updateHandler._updateScripts.length).to.equal(cfg.dbVersion); + }); + }); + + describe('#update', function() { + var versionDbType = 'dbVersion'; + + it('should not update when up to date', function(done) { + cfg.dbVersion = 3; // app requires database version 3 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, '3'); // database version is 3 + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + + done(); + }); + }); + + describe('dummy updates for v0 through v4', function() { + var updateCounter; + + beforeEach(function() { + updateCounter = 0; + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(); // database version is 0 + }); + + afterEach(function() { + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + + it('should work', function(done) { + cfg.dbVersion = 4; // app requires database version 4 + + // a simple dummy update to executed that only increments the update counter + function dummyUpdate(options, callback) { + updateCounter++; + callback(); + } + + // inject the dummy updates instead of live ones + updateHandler._updateScripts = [dummyUpdate, dummyUpdate, dummyUpdate, dummyUpdate]; + + // execute test + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(updateCounter).to.equal(4); + + done(); + }); + }); + + it('should fail while updating to v3', function(done) { + cfg.dbVersion = 4; // app requires database version 4 + + function dummyUpdate(options, callback) { + updateCounter++; + callback(); + } + + function failingUpdate(options, callback) { + updateCounter++; + callback({}); + } + + // inject the dummy updates instead of live ones + updateHandler._updateScripts = [dummyUpdate, dummyUpdate, failingUpdate, dummyUpdate]; + + // execute test + updateHandler.update(function(error) { + expect(error).to.exist; + expect(updateCounter).to.equal(3); + + done(); + }); + }); + + }); + + describe('v0 -> v1', function() { + var emailDbType = 'email_'; + + beforeEach(function() { + cfg.dbVersion = 1; // app requires database version 1 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(); // database version is 0 + }); + + afterEach(function() { + // database version is only queried for version checking prior to the update script + // so no need to check this in case-specific tests + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + it('should work', function(done) { + userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([1], versionDbType).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting database version fails', function(done) { + userStorageStub.removeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync({}); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when wiping emails from database fails', function(done) { + userStorageStub.removeList.yieldsAsync({}); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + }); + }); + }); +}); \ No newline at end of file