mirror of
https://github.com/moparisthebest/mail
synced 2024-11-11 03:35:01 -05:00
Merge pull request #212 from whiteout-io/dev/WO-767
[WO-767] Initialize lawnchair asynchronously during startup
This commit is contained in:
commit
51715822f8
@ -9,31 +9,36 @@ var LoginCtrl = function($scope, $timeout, $location, updateHandler, account, au
|
|||||||
|
|
||||||
function initializeUser() {
|
function initializeUser() {
|
||||||
// init the auth modules
|
// init the auth modules
|
||||||
auth.init();
|
auth.init(function(err) {
|
||||||
// get OAuth token from chrome
|
|
||||||
auth.getEmailAddress(function(err, info) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
dialog.error(err);
|
return dialog.error(err);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if account needs to be selected
|
// get OAuth token from chrome
|
||||||
if (!info.emailAddress) {
|
auth.getEmailAddress(function(err, info) {
|
||||||
$scope.goTo('/add-account');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiate the account by initializing the email dao and user storage
|
|
||||||
account.init({
|
|
||||||
emailAddress: info.emailAddress,
|
|
||||||
realname: info.realname
|
|
||||||
}, function(err, availableKeys) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
dialog.error(err);
|
dialog.error(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(availableKeys);
|
// check if account needs to be selected
|
||||||
|
if (!info.emailAddress) {
|
||||||
|
$scope.goTo('/add-account');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initiate the account by initializing the email dao and user storage
|
||||||
|
account.init({
|
||||||
|
emailAddress: info.emailAddress,
|
||||||
|
realname: info.realname
|
||||||
|
}, function(err, availableKeys) {
|
||||||
|
if (err) {
|
||||||
|
dialog.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(availableKeys);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -60,20 +60,20 @@ Account.prototype.init = function(options, callback) {
|
|||||||
|
|
||||||
// Pre-Flight check: initialize and prepare user's local database
|
// Pre-Flight check: initialize and prepare user's local database
|
||||||
function prepareDatabase() {
|
function prepareDatabase() {
|
||||||
try {
|
self._accountStore.init(options.emailAddress, function(err) {
|
||||||
self._accountStore.init(options.emailAddress);
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate the databases if necessary
|
|
||||||
self._updateHandler.update(function(err) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message));
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareKeys();
|
// Migrate the databases if necessary
|
||||||
|
self._updateHandler.update(function(err) {
|
||||||
|
if (err) {
|
||||||
|
return callback(new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareKeys();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ var axe = require('axe-logger'),
|
|||||||
cfg = require('../app-config').config,
|
cfg = require('../app-config').config,
|
||||||
str = require('../app-config').string;
|
str = require('../app-config').string;
|
||||||
|
|
||||||
|
var APP_CONFIG_DB_NAME = 'app-config';
|
||||||
var EMAIL_ADDR_DB_KEY = 'emailaddress';
|
var EMAIL_ADDR_DB_KEY = 'emailaddress';
|
||||||
var USERNAME_DB_KEY = 'username';
|
var USERNAME_DB_KEY = 'username';
|
||||||
var REALNAME_DB_KEY = 'realname';
|
var REALNAME_DB_KEY = 'realname';
|
||||||
@ -34,8 +35,13 @@ function Auth(appConfigStore, oauth, pgp) {
|
|||||||
/**
|
/**
|
||||||
* Initialize the service
|
* Initialize the service
|
||||||
*/
|
*/
|
||||||
Auth.prototype.init = function() {
|
Auth.prototype.init = function(callback) {
|
||||||
this._initialized = true;
|
var self = this;
|
||||||
|
|
||||||
|
self._appConfigStore.init(APP_CONFIG_DB_NAME, function(error) {
|
||||||
|
self._initialized = !error;
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,9 +4,7 @@ var ngModule = angular.module('woServices');
|
|||||||
|
|
||||||
// expose an instance with the static dbName 'app-config' to store configuration data
|
// expose an instance with the static dbName 'app-config' to store configuration data
|
||||||
ngModule.factory('appConfigStore', function(appConfigLawnchair) {
|
ngModule.factory('appConfigStore', function(appConfigLawnchair) {
|
||||||
var deviceStorage = new DeviceStorage(appConfigLawnchair);
|
return new DeviceStorage(appConfigLawnchair);
|
||||||
deviceStorage.init('app-config');
|
|
||||||
return deviceStorage;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// expose a singleton instance of DeviceStorage called 'accountStore' to persist user data
|
// expose a singleton instance of DeviceStorage called 'accountStore' to persist user data
|
||||||
@ -31,8 +29,8 @@ function DeviceStorage(lawnchairDAO) {
|
|||||||
* Initialize the lawnchair database
|
* Initialize the lawnchair database
|
||||||
* @param {String} dbName The name of the database
|
* @param {String} dbName The name of the database
|
||||||
*/
|
*/
|
||||||
DeviceStorage.prototype.init = function(dbName) {
|
DeviceStorage.prototype.init = function(dbName, callback) {
|
||||||
this._lawnchairDAO.init(dbName);
|
this._lawnchairDAO.init(dbName, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,13 +14,17 @@ function LawnchairDAO() {}
|
|||||||
* Initialize the lawnchair database
|
* Initialize the lawnchair database
|
||||||
* @param {String} dbName The name of the database
|
* @param {String} dbName The name of the database
|
||||||
*/
|
*/
|
||||||
LawnchairDAO.prototype.init = function(dbName) {
|
LawnchairDAO.prototype.init = function(dbName, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
throw new Error('Lawnchair DB name must be specified!');
|
return callback(new Error('Lawnchair DB name must be specified!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._db = new Lawnchair({
|
self._db = new Lawnchair({
|
||||||
name: dbName
|
name: dbName
|
||||||
|
}, function(success) {
|
||||||
|
callback(success ? undefined : new Error('Lawnchair initialization ' + dbName + ' failed!'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ var ImapClient = require('imap-client'),
|
|||||||
describe('Email DAO integration tests', function() {
|
describe('Email DAO integration tests', function() {
|
||||||
this.timeout(100000);
|
this.timeout(100000);
|
||||||
|
|
||||||
var accountService, emailDao, imapClient, imapMessages, imapFolders, imapServer, smtpServer, smtpClient, userStorage,
|
var accountService, emailDao, imapClient, imapMessages, imapFolders, imapServer, smtpServer, smtpClient, userStorage, auth,
|
||||||
mockKeyPair, inbox, spam;
|
mockKeyPair, inbox, spam;
|
||||||
|
|
||||||
var testAccount = {
|
var testAccount = {
|
||||||
@ -252,58 +252,67 @@ describe('Email DAO integration tests', function() {
|
|||||||
|
|
||||||
// clear the local database before each test
|
// clear the local database before each test
|
||||||
var cleanup = new DeviceStorageDAO(new LawnchairDAO());
|
var cleanup = new DeviceStorageDAO(new LawnchairDAO());
|
||||||
cleanup.init(testAccount.user);
|
cleanup.init(testAccount.user, function() {
|
||||||
cleanup.clear(function(err) {
|
cleanup.clear(function(err) {
|
||||||
expect(err).to.not.exist;
|
|
||||||
|
|
||||||
userStorage = accountService._accountStore;
|
|
||||||
|
|
||||||
accountService.init({
|
|
||||||
emailAddress: testAccount.user
|
|
||||||
}, function(err) {
|
|
||||||
expect(err).to.not.exist;
|
expect(err).to.not.exist;
|
||||||
|
|
||||||
emailDao = accountService._emailDao;
|
onCleaned();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// stub rest request to key server
|
function onCleaned() {
|
||||||
sinon.stub(emailDao._keychain._publicKeyDao, 'get').yields(null, mockKeyPair.publicKey);
|
userStorage = accountService._accountStore;
|
||||||
sinon.stub(emailDao._keychain._publicKeyDao, 'getByUserId').yields(null, mockKeyPair.publicKey);
|
auth = accountService._auth;
|
||||||
|
|
||||||
emailDao.onIncomingMessage = function(messages) {
|
auth.init(function(err) {
|
||||||
expect(messages.length).to.equal(imapMessages.length);
|
expect(err).to.not.exist;
|
||||||
inbox = emailDao._account.folders.filter(function(folder) {
|
accountService.init({
|
||||||
return folder.path === 'INBOX';
|
emailAddress: testAccount.user
|
||||||
}).pop();
|
|
||||||
spam = emailDao._account.folders.filter(function(folder) {
|
|
||||||
return folder.path === '[Gmail]/Spam';
|
|
||||||
}).pop();
|
|
||||||
expect(inbox).to.exist;
|
|
||||||
expect(spam).to.exist;
|
|
||||||
|
|
||||||
inbox.messages.sort(function(a, b) {
|
|
||||||
return a.uid - b.uid;
|
|
||||||
});
|
|
||||||
|
|
||||||
// phantomjs is really slow, so setting the tcp socket timeouts to 200s will effectively disarm the timeout
|
|
||||||
imapClient._client.client.TIMEOUT_SOCKET_LOWER_BOUND = 999999999;
|
|
||||||
imapClient._listeningClient.client.TIMEOUT_SOCKET_LOWER_BOUND = 999999999;
|
|
||||||
smtpClient.TIMEOUT_SOCKET_LOWER_BOUND = 999999999;
|
|
||||||
|
|
||||||
done();
|
|
||||||
};
|
|
||||||
|
|
||||||
emailDao.unlock({
|
|
||||||
passphrase: testAccount.pass,
|
|
||||||
keypair: mockKeyPair
|
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
expect(err).to.not.exist;
|
expect(err).to.not.exist;
|
||||||
|
|
||||||
accountService.onConnect(function(err) {
|
emailDao = accountService._emailDao;
|
||||||
|
|
||||||
|
// stub rest request to key server
|
||||||
|
sinon.stub(emailDao._keychain._publicKeyDao, 'get').yields(null, mockKeyPair.publicKey);
|
||||||
|
sinon.stub(emailDao._keychain._publicKeyDao, 'getByUserId').yields(null, mockKeyPair.publicKey);
|
||||||
|
|
||||||
|
emailDao.onIncomingMessage = function(messages) {
|
||||||
|
expect(messages.length).to.equal(imapMessages.length);
|
||||||
|
inbox = emailDao._account.folders.filter(function(folder) {
|
||||||
|
return folder.path === 'INBOX';
|
||||||
|
}).pop();
|
||||||
|
spam = emailDao._account.folders.filter(function(folder) {
|
||||||
|
return folder.path === '[Gmail]/Spam';
|
||||||
|
}).pop();
|
||||||
|
expect(inbox).to.exist;
|
||||||
|
expect(spam).to.exist;
|
||||||
|
|
||||||
|
inbox.messages.sort(function(a, b) {
|
||||||
|
return a.uid - b.uid;
|
||||||
|
});
|
||||||
|
|
||||||
|
// phantomjs is really slow, so setting the tcp socket timeouts to 200s will effectively disarm the timeout
|
||||||
|
imapClient._client.client.TIMEOUT_SOCKET_LOWER_BOUND = 999999999;
|
||||||
|
imapClient._listeningClient.client.TIMEOUT_SOCKET_LOWER_BOUND = 999999999;
|
||||||
|
smtpClient.TIMEOUT_SOCKET_LOWER_BOUND = 999999999;
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
emailDao.unlock({
|
||||||
|
passphrase: testAccount.pass,
|
||||||
|
keypair: mockKeyPair
|
||||||
|
}, function(err) {
|
||||||
expect(err).to.not.exist;
|
expect(err).to.not.exist;
|
||||||
|
|
||||||
|
accountService.onConnect(function(err) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ describe('Login Controller unit test', function() {
|
|||||||
afterEach(function() {});
|
afterEach(function() {});
|
||||||
|
|
||||||
it('should fail for auth.getEmailAddress', function() {
|
it('should fail for auth.getEmailAddress', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(new Error());
|
authMock.getEmailAddress.yields(new Error());
|
||||||
|
|
||||||
createController();
|
createController();
|
||||||
@ -63,7 +64,21 @@ describe('Login Controller unit test', function() {
|
|||||||
expect(dialogMock.error.calledOnce).to.be.true;
|
expect(dialogMock.error.calledOnce).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail for auth.init', function() {
|
||||||
|
authMock.init.yields(new Error());
|
||||||
|
authMock.getEmailAddress.yields(null, {
|
||||||
|
emailAddress: emailAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
createController();
|
||||||
|
|
||||||
|
expect(authMock.init.calledOnce).to.be.true;
|
||||||
|
expect(accountMock.init.called).to.be.false;
|
||||||
|
expect(dialogMock.error.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
it('should redirect to /add-account', function() {
|
it('should redirect to /add-account', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {});
|
authMock.getEmailAddress.yields(null, {});
|
||||||
|
|
||||||
createController();
|
createController();
|
||||||
@ -71,19 +86,8 @@ describe('Login Controller unit test', function() {
|
|||||||
expect(goToStub.withArgs('/add-account').calledOnce).to.be.true;
|
expect(goToStub.withArgs('/add-account').calledOnce).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail for auth.init', function() {
|
|
||||||
authMock.getEmailAddress.yields(null, {
|
|
||||||
emailAddress: emailAddress
|
|
||||||
});
|
|
||||||
accountMock.init.yields(new Error());
|
|
||||||
|
|
||||||
createController();
|
|
||||||
|
|
||||||
expect(accountMock.init.calledOnce).to.be.true;
|
|
||||||
expect(dialogMock.error.calledOnce).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to /login-existing', function() {
|
it('should redirect to /login-existing', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
@ -99,6 +103,7 @@ describe('Login Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fail for auth.storeCredentials', function() {
|
it('should fail for auth.storeCredentials', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
@ -115,6 +120,7 @@ describe('Login Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to /desktop', function() {
|
it('should redirect to /desktop', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
@ -131,6 +137,7 @@ describe('Login Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fail for keychain.requestPrivateKeyDownload', function() {
|
it('should fail for keychain.requestPrivateKeyDownload', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
@ -145,6 +152,7 @@ describe('Login Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to /login-privatekey-download', function() {
|
it('should redirect to /login-privatekey-download', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
@ -159,6 +167,7 @@ describe('Login Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to /login-new-device', function() {
|
it('should redirect to /login-new-device', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
@ -173,6 +182,7 @@ describe('Login Controller unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to /login-initial', function() {
|
it('should redirect to /login-initial', function() {
|
||||||
|
authMock.init.yields();
|
||||||
authMock.getEmailAddress.yields(null, {
|
authMock.getEmailAddress.yields(null, {
|
||||||
emailAddress: emailAddress
|
emailAddress: emailAddress
|
||||||
});
|
});
|
||||||
|
@ -67,7 +67,7 @@ describe('Account Service unit test', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fail for _accountStore.init', function() {
|
it('should fail for _accountStore.init', function() {
|
||||||
devicestorageStub.init.throws(new Error('asdf'));
|
devicestorageStub.init.yields(new Error('asdf'));
|
||||||
|
|
||||||
account.init({
|
account.init({
|
||||||
emailAddress: dummyUser,
|
emailAddress: dummyUser,
|
||||||
|
@ -13,6 +13,8 @@ describe('Auth unit tests', function() {
|
|||||||
var PASSWD_DB_KEY = 'password';
|
var PASSWD_DB_KEY = 'password';
|
||||||
var IMAP_DB_KEY = 'imap';
|
var IMAP_DB_KEY = 'imap';
|
||||||
var SMTP_DB_KEY = 'smtp';
|
var SMTP_DB_KEY = 'smtp';
|
||||||
|
var APP_CONFIG_DB_NAME = 'app-config';
|
||||||
|
|
||||||
// SUT
|
// SUT
|
||||||
var auth;
|
var auth;
|
||||||
|
|
||||||
@ -46,6 +48,25 @@ describe('Auth unit tests', function() {
|
|||||||
auth = new Auth(storageStub, oauthStub, pgpStub);
|
auth = new Auth(storageStub, oauthStub, pgpStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#init', function() {
|
||||||
|
it('should initialize a user db', function(done) {
|
||||||
|
storageStub.init.withArgs(APP_CONFIG_DB_NAME).yields();
|
||||||
|
auth.init(function(err) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(auth._initialized).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should initialize a user db', function(done) {
|
||||||
|
storageStub.init.withArgs(APP_CONFIG_DB_NAME).yields(new Error());
|
||||||
|
auth.init(function(err) {
|
||||||
|
expect(err).to.exist;
|
||||||
|
expect(auth._initialized).to.be.false;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#getCredentials', function() {
|
describe('#getCredentials', function() {
|
||||||
it('should load credentials and retrieve credentials from cfg', function(done) {
|
it('should load credentials and retrieve credentials from cfg', function(done) {
|
||||||
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]);
|
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]);
|
||||||
|
@ -18,8 +18,21 @@ describe('Device Storage DAO unit tests', function() {
|
|||||||
|
|
||||||
describe('init', function() {
|
describe('init', function() {
|
||||||
it('should work', function() {
|
it('should work', function() {
|
||||||
storageDao.init(testUser);
|
lawnchairDaoStub.init.yields();
|
||||||
expect(lawnchairDaoStub.init.calledOnce).to.be.true;
|
|
||||||
|
storageDao.init(testUser, function(err) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(lawnchairDaoStub.init.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail', function() {
|
||||||
|
lawnchairDaoStub.init.yields(new Error());
|
||||||
|
|
||||||
|
storageDao.init(testUser, function(err) {
|
||||||
|
expect(err).to.exist;
|
||||||
|
expect(lawnchairDaoStub.init.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -20,10 +20,13 @@ var data2 = {
|
|||||||
describe('Lawnchair DAO unit tests', function() {
|
describe('Lawnchair DAO unit tests', function() {
|
||||||
var lawnchairDao;
|
var lawnchairDao;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function(done) {
|
||||||
lawnchairDao = new LawnchairDAO();
|
lawnchairDao = new LawnchairDAO();
|
||||||
lawnchairDao.init(dbName);
|
lawnchairDao.init(dbName, function(err) {
|
||||||
expect(lawnchairDao._db).to.exist;
|
expect(err).to.not.exist;
|
||||||
|
expect(lawnchairDao._db).to.exist;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function(done) {
|
afterEach(function(done) {
|
||||||
|
Loading…
Reference in New Issue
Block a user