Merge pull request #130 from whiteout-io/WO-563

[WO-563] Introduce connection doctor
This commit is contained in:
Tankred Hase 2014-09-23 17:40:50 +02:00
commit 6be8b71247
9 changed files with 793 additions and 160 deletions

View File

@ -177,7 +177,8 @@ define(function(require) {
appVersion: appVersion,
outboxMailboxPath: 'OUTBOX',
outboxMailboxName: 'Outbox',
outboxMailboxType: 'Outbox'
outboxMailboxType: 'Outbox',
connDocTimeout: 5000,
};
/**
@ -207,7 +208,14 @@ define(function(require) {
bugReportTitle: 'Report a bug',
bugReportSubject: '[Bug] I want to report a bug',
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider in an anonymized way from the point where you started the app for the last time. Any information provided by you will be used for the porpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\n',
supportAddress: 'mail.support@whiteout.io'
supportAddress: 'mail.support@whiteout.io',
connDocOffline: 'It appears that you are offline. Please retry when you are online.',
connDocTlsWrongCert: 'A connection to {0} was rejected because the TLS certificate is invalid. Please have a look at the FAQ for information on how to fix this error.',
connDocHostUnreachable: 'We could not establish a connection to {0}. Please check the server settings!',
connDocHostTimeout: 'We could not establish a connection to {0} within {1} ms. Please check the server settings and encryption mode!',
connDocAuthRejected: 'Your credentials for {0} were rejected. Please check your username and password!',
connDocNoInbox: 'We could not detect an IMAP inbox folder on {0}. Please have a look at the FAQ for information on how to fix this error.',
connDocGenericError: 'There was an error connecting to {0}: {1}'
};
return app;

View File

@ -24,6 +24,7 @@ define(function(require) {
PrivateKeyDAO = require('js/dao/privatekey-dao'),
InvitationDAO = require('js/dao/invitation-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
ConnectionDoctor = require('js/util/connection-doctor'),
UpdateHandler = require('js/util/update/update-handler'),
config = appConfig.config,
str = appConfig.string;
@ -104,6 +105,7 @@ define(function(require) {
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth);
self._adminDao = new AdminDao(new RestDAO(config.adminUrl));
self._doctor = new ConnectionDoctor();
emailDao.onError = self.onError;
};

View File

@ -6,21 +6,22 @@ define(function(require) {
var ENCRYPTION_METHOD_TLS = 2;
var appCtrl = require('js/app-controller'),
config = require('js/app-config').config,
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient');
config = require('js/app-config').config;
var SetCredentialsCtrl = function($scope, $location, $routeParams) {
if (!appCtrl._emailDao && !$routeParams.dev) {
if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app
return;
}
var auth = appCtrl._auth;
var doctor = appCtrl._doctor;
var provider;
//
// Presets and Settings
//
provider = $location.search().provider;
var provider = $location.search().provider;
$scope.hasProviderPreset = !!config[provider];
$scope.useOAuth = !!auth.oauthToken;
$scope.showDetails = (provider === 'custom');
@ -30,12 +31,15 @@ define(function(require) {
}
if ($scope.hasProviderPreset) {
// use non-editable smtp and imap presets for provider
// use non-editable presets
// SMTP config
$scope.smtpHost = config[provider].smtp.host;
$scope.smtpPort = config[provider].smtp.port;
$scope.smtpCert = config[provider].smtp.ca;
$scope.smtpPinned = config[provider].smtp.pinned;
// transport encryption method
if (config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) {
$scope.smtpEncryption = ENCRYPTION_METHOD_TLS;
} else if (!config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) {
@ -44,11 +48,13 @@ define(function(require) {
$scope.smtpEncryption = ENCRYPTION_METHOD_NONE;
}
// IMAP config
$scope.imapHost = config[provider].imap.host;
$scope.imapPort = config[provider].imap.port;
$scope.imapCert = config[provider].imap.ca;
$scope.imapPinned = config[provider].imap.pinned;
// transport encryption method
if (config[provider].imap.secure && !config[provider].imap.ignoreTLS) {
$scope.imapEncryption = ENCRYPTION_METHOD_TLS;
} else if (!config[provider].imap.secure && !config[provider].imap.ignoreTLS) {
@ -58,109 +64,19 @@ define(function(require) {
}
}
$scope.test = function(imapClient, smtpClient) {
var imapEncryption = parseInt($scope.imapEncryption, 10);
var smtpEncryption = parseInt($scope.smtpEncryption, 10);
$scope.credentialsIncomplete = false;
$scope.connectionError = false;
$scope.smtpOk = undefined;
$scope.imapOk = undefined;
if (!(($scope.username || $scope.emailAddress) && ($scope.password || $scope.useOAuth))) {
$scope.credentialsIncomplete = true;
return;
}
var imap = imapClient || new ImapClient({
host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort,
secure: imapEncryption === ENCRYPTION_METHOD_TLS,
ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE,
ca: $scope.imapCert,
auth: {
user: $scope.username || $scope.emailAddress,
pass: $scope.password,
xoauth2: auth.oauthToken
}
});
imap.onCert = function(pemEncodedCert) {
if (!$scope.imapPinned) {
$scope.imapCert = pemEncodedCert;
}
};
imap.onError = function(err) {
$scope.imapOk = !err;
$scope.connectionError = err;
done();
};
var smtp = smtpClient || new SmtpClient($scope.smtpHost.toLowerCase(), $scope.smtpPort, {
useSecureTransport: smtpEncryption === ENCRYPTION_METHOD_TLS,
ignoreTLS: smtpEncryption === ENCRYPTION_METHOD_NONE,
ca: $scope.smtpCert,
auth: {
user: $scope.username || $scope.emailAddress,
pass: $scope.password,
xoauth2: auth.oauthToken
}
});
smtp.oncert = function(pemEncodedCert) {
if (!$scope.smtpPinned) {
$scope.smtpCert = pemEncodedCert;
}
};
smtp.onerror = function(err) {
$scope.smtpOk = !err;
$scope.connectionError = $scope.connectionError || err;
done();
};
smtp.onidle = function() {
smtp.onerror = function() {}; // don't care about errors after discarding connection
$scope.smtpOk = true;
smtp.quit();
done();
};
$scope.busy = 2;
// fire away
imap.login(function(err) {
$scope.connectionError = $scope.connectionError || err;
$scope.imapOk = !err;
imap.logout(function() {}); // don't care about errors after discarding connection
done();
});
smtp.connect();
};
function done() {
if ($scope.busy > 0) {
$scope.busy--;
}
if ($scope.smtpOk && $scope.imapOk) {
login();
}
$scope.$apply();
}
function login() {
$scope.test = function() {
// parse the <select> dropdown lists
var imapEncryption = parseInt($scope.imapEncryption, 10);
var smtpEncryption = parseInt($scope.smtpEncryption, 10);
auth.setCredentials({
// build credentials object
var credentials = {
provider: provider,
emailAddress: $scope.emailAddress,
username: $scope.username || $scope.emailAddress,
realname: $scope.realname,
password: $scope.password,
xoauth2: auth.oauthToken,
imap: {
host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort,
@ -177,13 +93,27 @@ define(function(require) {
ca: $scope.smtpCert,
pinned: !!$scope.smtpPinned
}
});
redirect();
}
};
function redirect() {
$location.path('/login');
}
// use the credentials in the connection doctor
doctor.configure(credentials);
// run connection doctor test suite
$scope.busy = true;
doctor.check(function(err) {
if (err) {
// display the error in the settings UI
$scope.connectionError = err;
} else {
// persists the credentials and forwards to /login
auth.setCredentials(credentials);
$location.path('/login');
}
$scope.busy = false;
$scope.$apply();
});
};
};
return SetCredentialsCtrl;

View File

@ -0,0 +1,289 @@
define(function(require) {
'use strict';
var TCPSocket = require('tcp-socket'),
appConfig = require('js/app-config'),
cfg = appConfig.config,
strings = appConfig.string,
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient');
/**
* The connection doctor can check your connection. In essence, it reconstructs what happens when
* the app goes online in an abbreviated way. You need to configure() the instance with the IMAP/SMTP
* credentials before running check()!
*
* @constructor
*/
var ConnectionDoctor = function() {};
//
// Error codes
//
var OFFLINE = ConnectionDoctor.OFFLINE = 42;
var TLS_WRONG_CERT = ConnectionDoctor.TLS_WRONG_CERT = 43;
var HOST_UNREACHABLE = ConnectionDoctor.HOST_UNREACHABLE = 44;
var HOST_TIMEOUT = ConnectionDoctor.HOST_TIMEOUT = 45;
var AUTH_REJECTED = ConnectionDoctor.AUTH_REJECTED = 46;
var NO_INBOX = ConnectionDoctor.NO_INBOX = 47;
var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48;
//
// Public API
//
/**
* Configures the connection doctor
*
* @param {Object} credentials.imap IMAP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean)
* @param {Object} credentials.smtp SMTP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean)
* @param {String} credentials.username
* @param {String} credentials.password
*/
ConnectionDoctor.prototype.configure = function(credentials) {
this.credentials = credentials;
// internal members
this._imap = new ImapClient({
host: this.credentials.imap.host,
port: this.credentials.imap.port,
secure: this.credentials.imap.secure,
ignoreTLS: this.credentials.imap.ignoreTLS,
ca: this.credentials.imap.ca,
auth: {
user: this.credentials.username,
pass: this.credentials.password,
xoauth2: this.credentials.xoauth2
}
});
this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, {
useSecureTransport: this.credentials.smtp.secure,
ignoreTLS: this.credentials.smtp.ignoreTLS,
ca: this.credentials.smtp.ca,
auth: {
user: this.credentials.username,
pass: this.credentials.password,
xoauth2: this.credentials.xoauth2
}
});
};
/**
* It conducts the following tests for IMAP and SMTP, respectively:
* 1) Check if browser is online
* 2) Connect to host:port via TCP/TLS
* 3) Login to the server
* 4) Perform some basic commands (e.g. list folders)
* 5) Exposes error codes
*
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
*/
ConnectionDoctor.prototype.check = function(callback) {
var self = this;
if (!self.credentials) {
return callback(new Error('You need to configure() the connection doctor first!'));
}
self._checkOnline(function(error) {
if (error) {
return callback(error);
}
self._checkReachable(self.credentials.imap, function(error) {
if (error) {
return callback(error);
}
self._checkReachable(self.credentials.smtp, function(error) {
if (error) {
return callback(error);
}
self._checkImap(function(error) {
if (error) {
return callback(error);
}
self._checkSmtp(callback);
});
});
});
});
};
//
// Internal API
//
/**
* Checks if the browser is online
*
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if browser is offline
*/
ConnectionDoctor.prototype._checkOnline = function(callback) {
if (navigator.onLine) {
callback();
} else {
callback(createError(OFFLINE, strings.connDocOffline));
}
};
/**
* Checks if a host is reachable via TCP
*
* @param {String} options.host
* @param {Number} options.port
* @param {Boolean} options.secure
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
*/
ConnectionDoctor.prototype._checkReachable = function(options, callback) {
var socket,
error, // remember the error message
timeout, // remember the timeout object
host = options.host + ':' + options.port,
hasTimedOut = false; // prevents multiple callbacks
timeout = setTimeout(function() {
hasTimedOut = true;
callback(createError(HOST_TIMEOUT, strings.connDocHostTimeout.replace('{0}', host).replace('{1}', cfg.connDocTimeout)));
}, cfg.connDocTimeout);
socket = TCPSocket.open(options.host, options.port, {
binaryType: 'arraybuffer',
useSecureTransport: options.secure,
ca: options.ca
});
socket.ondata = function() {}; // we don't actually care about the data
socket.oncert = function() {
if (options.ca) {
// the certificate we already have is outdated
error = createError(TLS_WRONG_CERT, strings.connDocTlsWrongCert.replace('{0}', host));
}
};
socket.onerror = function(e) {
if (!error) {
error = createError(HOST_UNREACHABLE, strings.connDocHostUnreachable.replace('{0}', host), e.data);
}
};
socket.onopen = function() {
socket.close();
};
socket.onclose = function() {
if (!hasTimedOut) {
clearTimeout(timeout);
callback(error);
}
};
};
/**
* Checks if an IMAP server is reachable, accepts the credentials, can list folders and has an inbox and logs out.
* Adds the certificate to the IMAP settings if not provided.
*
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
*/
ConnectionDoctor.prototype._checkImap = function(callback) {
var self = this,
loggedIn = false,
host = self.credentials.imap.host + ':' + self.credentials.imap.port;
self._imap.onCert = function(pemEncodedCert) {
if (!self.credentials.imap.ca) {
self.credentials.imap.ca = pemEncodedCert;
}
};
// login and logout do not use error objects in the callback, but rather invoke
// the global onError handler, so we need to track if login was successful
self._imap.onError = function(error) {
if (!loggedIn) {
callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error));
} else {
callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
}
};
self._imap.login(function() {
loggedIn = true;
self._imap.listWellKnownFolders(function(error, wellKnownFolders) {
if (error) {
return callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
}
if (wellKnownFolders.Inbox.length === 0) {
// the client needs at least an inbox folder to work properly
return callback(createError(NO_INBOX, strings.connDocNoInbox.replace('{0}', host)));
}
self._imap.logout(function() {
callback();
});
});
});
};
/**
* Checks if an SMTP server is reachable and accepts the credentials and logs out.
* Adds the certificate to the SMTP settings if not provided.
*
* @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong
*/
ConnectionDoctor.prototype._checkSmtp = function(callback) {
var self = this,
host = self.credentials.smtp.host + ':' + self.credentials.smtp.port,
errored = false; // tracks if we need to invoke the callback at onclose or not
self._smtp.oncert = function(pemEncodedCert) {
if (!self.credentials.smtp.ca) {
self.credentials.smtp.ca = pemEncodedCert;
}
};
self._smtp.onerror = function(error) {
if (error) {
errored = true;
callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error));
}
};
self._smtp.onidle = function() {
self._smtp.quit();
};
self._smtp.onclose = function() {
if (!errored) {
callback();
}
};
self._smtp.connect();
};
//
// Helper Functions
//
function createError(code, message, underlyingError) {
var error = new Error(message);
error.code = code;
error.underlyingError = underlyingError;
return error;
}
return ConnectionDoctor;
});

View File

@ -164,6 +164,14 @@
text-decoration: none;
}
.connection-error {
margin: 30px 0;
p {
margin-bottom: 15px;
}
}
.details {
fieldset {
margin: 0 0 10px 0;

View File

@ -11,7 +11,13 @@
<form name="form">
<div>
<label class="input-error-message" ng-show="connectionError">Connection failed. Please check your credentials!</label>
<fieldset class="connection-error" ng-show="connectionError">
<legend>Connection Error</legend>
<p class="input-error-message">{{connectionError.message}}</p>
<p ng-show="connectionError.underlyingError" class="input-error-message">Underlying Cause: {{connectionError.underlyingError.message}}</p>
<a class="input-error-message" href="https://github.com/whiteout-io/mail-html5/wiki/FAQ#troubleshooting" target="_blank">Find out more in the FAQ.</a>
</fieldset>
<label class="input-error-message" ng-show="form.$invalid || credentialsIncomplete">Please fill out all required fields!</label>
<br>
<input class="input-text" type="email" required ng-model="emailAddress" placeholder="Email address" focus-me="true" tabindex="1" spellcheck="false"></input>

View File

@ -0,0 +1,390 @@
define(function(require) {
'use strict';
var ConnectionDoctor = require('js/util/connection-doctor'),
TCPSocket = require('tcp-socket'),
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient'),
cfg = require('js/app-config').config,
expect = chai.expect;
describe('Connection Doctor', function() {
var doctor;
var socketStub, imapStub, smtpStub, credentials;
beforeEach(function() {
//
// Stubs
//
// there is no socket shim for for this use case, use dummy object
socketStub = {
close: function() {
this.onclose();
}
};
imapStub = sinon.createStubInstance(ImapClient);
smtpStub = sinon.createStubInstance(SmtpClient);
//
// Fixture
//
credentials = {
imap: {
host: 'asd',
port: 1234,
secure: true,
ca: 'cert'
},
smtp: {
host: 'qwe',
port: 5678,
secure: false,
ca: 'cert'
},
username: 'username',
password: 'password'
};
sinon.stub(TCPSocket, 'open').returns(socketStub); // convenience constructors suck
//
// Setup SUT
//
doctor = new ConnectionDoctor();
doctor.configure(credentials);
doctor._imap = imapStub;
doctor._smtp = smtpStub;
});
afterEach(function() {
TCPSocket.open.restore();
});
describe('#_checkOnline', function() {
it('should check if browser is online', function(done) {
doctor._checkOnline(function(error) {
if (navigator.onLine) {
expect(error).to.not.exist;
} else {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.OFFLINE);
}
done();
});
});
});
describe('#_checkReachable', function() {
it('should be able to reach the host w/o cert', function(done) {
credentials.imap.ca = undefined;
doctor._checkReachable(credentials.imap, function(error) {
expect(error).to.not.exist;
expect(TCPSocket.open.calledOnce).to.be.true;
expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, {
binaryType: 'arraybuffer',
useSecureTransport: credentials.imap.secure,
ca: credentials.imap.ca
})).to.be.true;
done();
});
socketStub.oncert();
socketStub.onopen();
});
it('should fail w/ wrong cert', function(done) {
doctor._checkReachable(credentials.imap, function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.TLS_WRONG_CERT);
expect(TCPSocket.open.calledOnce).to.be.true;
expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, {
binaryType: 'arraybuffer',
useSecureTransport: credentials.imap.secure,
ca: credentials.imap.ca
})).to.be.true;
done();
});
socketStub.oncert();
socketStub.onerror();
socketStub.onclose();
});
it('should fail w/ host unreachable', function(done) {
doctor._checkReachable(credentials.imap, function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.HOST_UNREACHABLE);
expect(TCPSocket.open.calledOnce).to.be.true;
done();
});
socketStub.onerror({
data: new Error()
});
socketStub.onclose();
});
it('should fail w/ timeout', function(done) {
var origTimeout = cfg.connDocTimeout; // remember timeout from the config to reset it on done
cfg.connDocTimeout = 20; // set to 20ms for the test
doctor._checkReachable(credentials.imap, function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.HOST_TIMEOUT);
expect(TCPSocket.open.calledOnce).to.be.true;
cfg.connDocTimeout = origTimeout;
done();
});
});
});
describe('#_checkImap', function() {
it('should perform IMAP login, list folders, logout', function(done) {
imapStub.login.yieldsAsync();
imapStub.listWellKnownFolders.yieldsAsync(null, {
Inbox: [{}]
});
imapStub.logout.yieldsAsync();
doctor._checkImap(function(error) {
expect(error).to.not.exist;
expect(imapStub.login.calledOnce).to.be.true;
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
expect(imapStub.logout.calledOnce).to.be.true;
done();
});
});
it('should fail w/ generic error on logout', function(done) {
imapStub.login.yieldsAsync();
imapStub.listWellKnownFolders.yieldsAsync(null, {
Inbox: [{}]
});
doctor._checkImap(function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR);
expect(error.underlyingError).to.exist;
expect(imapStub.login.calledOnce).to.be.true;
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
expect(imapStub.logout.calledOnce).to.be.true;
done();
});
setTimeout(function() {
// this error is thrown while we're waiting for the logout
imapStub.onError(new Error());
}, 50);
});
it('should fail w/ generic error on inbox missing', function(done) {
imapStub.login.yieldsAsync();
imapStub.listWellKnownFolders.yieldsAsync(null, {
Inbox: []
});
doctor._checkImap(function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.NO_INBOX);
expect(imapStub.login.calledOnce).to.be.true;
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
expect(imapStub.logout.called).to.be.false;
done();
});
});
it('should fail w/ generic error on listing folders fails', function(done) {
imapStub.login.yieldsAsync();
imapStub.listWellKnownFolders.yieldsAsync(new Error());
doctor._checkImap(function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR);
expect(error.underlyingError).to.exist;
expect(imapStub.login.calledOnce).to.be.true;
expect(imapStub.listWellKnownFolders.calledOnce).to.be.true;
expect(imapStub.logout.called).to.be.false;
done();
});
});
it('should fail w/ auth rejected', function(done) {
doctor._checkImap(function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED);
expect(error.underlyingError).to.exist;
expect(imapStub.login.calledOnce).to.be.true;
expect(imapStub.listWellKnownFolders.called).to.be.false;
expect(imapStub.logout.called).to.be.false;
done();
});
setTimeout(function() {
// this error is thrown while we're waiting for the login
imapStub.onError(new Error());
}, 50);
});
});
describe('#_checkSmtp', function() {
it('should perform SMTP login, logout', function(done) {
doctor._checkSmtp(function(error) {
expect(error).to.not.exist;
expect(smtpStub.connect.calledOnce).to.be.true;
expect(smtpStub.quit.calledOnce).to.be.true;
done();
});
smtpStub.onidle();
smtpStub.onclose();
});
it('should fail w/ auth rejected', function(done) {
doctor._checkSmtp(function(error) {
expect(error).to.exist;
expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED);
expect(error.underlyingError).to.exist;
expect(smtpStub.connect.calledOnce).to.be.true;
expect(smtpStub.quit.called).to.be.false;
done();
});
smtpStub.onerror(new Error());
});
});
describe('#check', function() {
beforeEach(function() {
sinon.stub(doctor, '_checkOnline');
sinon.stub(doctor, '_checkReachable');
sinon.stub(doctor, '_checkImap');
sinon.stub(doctor, '_checkSmtp');
});
it('should perform all tests', function(done) {
doctor._checkOnline.yieldsAsync();
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync();
doctor._checkImap.yieldsAsync();
doctor._checkSmtp.yieldsAsync();
doctor.check(function(err) {
expect(err).to.not.exist;
expect(doctor._checkOnline.calledOnce).to.be.true;
expect(doctor._checkReachable.calledTwice).to.be.true;
expect(doctor._checkImap.calledOnce).to.be.true;
expect(doctor._checkSmtp.calledOnce).to.be.true;
done();
});
});
it('should fail for smtp', function(done) {
doctor._checkOnline.yieldsAsync();
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync();
doctor._checkImap.yieldsAsync();
doctor._checkSmtp.yieldsAsync(new Error());
doctor.check(function(err) {
expect(err).to.exist;
expect(doctor._checkOnline.calledOnce).to.be.true;
expect(doctor._checkReachable.calledTwice).to.be.true;
expect(doctor._checkImap.calledOnce).to.be.true;
expect(doctor._checkSmtp.calledOnce).to.be.true;
done();
});
});
it('should fail for imap', function(done) {
doctor._checkOnline.yieldsAsync();
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync();
doctor._checkImap.yieldsAsync(new Error());
doctor.check(function(err) {
expect(err).to.exist;
expect(doctor._checkOnline.calledOnce).to.be.true;
expect(doctor._checkReachable.calledTwice).to.be.true;
expect(doctor._checkImap.calledOnce).to.be.true;
expect(doctor._checkSmtp.called).to.be.false;
done();
});
});
it('should fail for smtp reachability', function(done) {
doctor._checkOnline.yieldsAsync();
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync();
doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(new Error());
doctor.check(function(err) {
expect(err).to.exist;
expect(doctor._checkOnline.calledOnce).to.be.true;
expect(doctor._checkReachable.calledTwice).to.be.true;
expect(doctor._checkImap.called).to.be.false;
expect(doctor._checkSmtp.called).to.be.false;
done();
});
});
it('should fail for imap reachability', function(done) {
doctor._checkOnline.yieldsAsync();
doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(new Error());
doctor.check(function(err) {
expect(err).to.exist;
expect(doctor._checkOnline.calledOnce).to.be.true;
expect(doctor._checkReachable.calledOnce).to.be.true;
expect(doctor._checkImap.called).to.be.false;
expect(doctor._checkSmtp.called).to.be.false;
done();
});
});
it('should fail for offline', function(done) {
doctor._checkOnline.yieldsAsync(new Error());
doctor.check(function(err) {
expect(err).to.exist;
expect(doctor._checkOnline.calledOnce).to.be.true;
expect(doctor._checkReachable.called).to.be.false;
expect(doctor._checkImap.called).to.be.false;
expect(doctor._checkSmtp.called).to.be.false;
done();
});
});
it('should fail w/o config', function(done) {
doctor.credentials = doctor._imap = doctor._smtp = undefined;
doctor.check(function(err) {
expect(err).to.exist;
expect(doctor._checkOnline.called).to.be.false;
expect(doctor._checkReachable.called).to.be.false;
expect(doctor._checkImap.called).to.be.false;
expect(doctor._checkSmtp.called).to.be.false;
done();
});
});
});
});
});

View File

@ -4,24 +4,29 @@ define(function(require) {
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient'),
Auth = require('js/bo/auth'),
ConnectionDoctor = require('js/util/connection-doctor'),
SetCredentialsCtrl = require('js/controller/login-set-credentials'),
appController = require('js/app-controller');
describe('Login (Set Credentials) Controller unit test', function() {
var scope, location, setCredentialsCtrl;
var imap, smtp;
var origAuth;
var provider = 'providerproviderprovider';
// Angular parameters
var scope, location, provider;
// Stubs
var auth, origAuth, doctor, origDoctor;
// SUT
var setCredentialsCtrl;
beforeEach(function() {
// remeber pre-test state to restore later
origAuth = appController._auth;
appController._auth = {};
imap = sinon.createStubInstance(ImapClient);
smtp = sinon.createStubInstance(SmtpClient);
origDoctor = appController._doctor;
auth = appController._auth = sinon.createStubInstance(Auth);
doctor = appController._doctor = sinon.createStubInstance(ConnectionDoctor);
// setup the controller
angular.module('setcredentialstest', []);
mocks.module('setcredentialstest');
mocks.inject(function($rootScope, $controller, $location) {
@ -40,14 +45,13 @@ define(function(require) {
});
afterEach(function() {
// restore pre-test state
appController._auth = origAuth;
appController._doctor = origDoctor;
});
describe('set credentials', function() {
it('should work', function(done) {
var imapCert = 'imapcertimapcertimapcertimapcertimapcertimapcert',
smtpCert = 'smtpcertsmtpcertsmtpcertsmtpcertsmtpcertsmtpcert';
it('should work', function() {
scope.emailAddress = 'emailemailemailemail';
scope.password = 'passwdpasswdpasswdpasswd';
scope.smtpHost = 'hosthosthost';
@ -58,44 +62,39 @@ define(function(require) {
scope.imapEncryption = '2'; // TLS
scope.realname = 'peter pan';
imap.login.yields();
appController._auth.setCredentials = function(args) {
expect(smtp.connect.calledOnce).to.be.true;
expect(imap.login.calledOnce).to.be.true;
expect(args).to.deep.equal({
provider: provider,
emailAddress: scope.emailAddress,
username: scope.username || scope.emailAddress,
realname: scope.realname,
password: scope.password,
imap: {
host: scope.imapHost.toLowerCase(),
port: scope.imapPort,
secure: true,
ignoreTLS: false,
ca: scope.imapCert,
pinned: false
},
smtp: {
host: scope.smtpHost.toLowerCase(),
port: scope.smtpPort,
secure: false,
ignoreTLS: false,
ca: scope.smtpCert,
pinned: false
}
});
done();
var expectedCredentials = {
provider: provider,
emailAddress: scope.emailAddress,
username: scope.username || scope.emailAddress,
realname: scope.realname,
password: scope.password,
xoauth2: undefined,
imap: {
host: scope.imapHost.toLowerCase(),
port: scope.imapPort,
secure: true,
ignoreTLS: false,
ca: undefined,
pinned: false
},
smtp: {
host: scope.smtpHost.toLowerCase(),
port: scope.smtpPort,
secure: false,
ignoreTLS: false,
ca: undefined,
pinned: false
}
};
scope.test(imap, smtp);
doctor.check.yields(); // synchronous yields!
imap.onCert(imapCert);
smtp.oncert(smtpCert);
scope.test();
smtp.onidle();
expect(doctor.check.calledOnce).to.be.true;
expect(doctor.configure.calledOnce).to.be.true;
expect(doctor.configure.calledWith(expectedCredentials)).to.be.true;
expect(auth.setCredentials.calledOnce).to.be.true;
});
});
});

View File

@ -103,7 +103,8 @@ function startTests() {
'test/unit/write-ctrl-test',
'test/unit/outbox-bo-test',
'test/unit/invitation-dao-test',
'test/unit/update-handler-test'
'test/unit/update-handler-test',
'test/unit/connection-doctor-test'
], function() {
//Tests loaded, run tests
mocha.run();