1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-26 02:42:17 -05:00

[WO-563] Introduce connection doctor

* Uses predefined error messages from the config
* Show error and underlying error in login screen
This commit is contained in:
Felix Hammerl 2014-09-20 20:47:32 +02:00 committed by Tankred Hase
parent 91528f993b
commit 77e0377fd1
8 changed files with 787 additions and 160 deletions

View File

@ -177,7 +177,8 @@ define(function(require) {
appVersion: appVersion, appVersion: appVersion,
outboxMailboxPath: 'OUTBOX', outboxMailboxPath: 'OUTBOX',
outboxMailboxName: 'Outbox', outboxMailboxName: 'Outbox',
outboxMailboxType: 'Outbox' outboxMailboxType: 'Outbox',
connDocTimeout: 5000,
}; };
/** /**
@ -207,7 +208,14 @@ define(function(require) {
bugReportTitle: 'Report a bug', bugReportTitle: 'Report a bug',
bugReportSubject: '[Bug] I want to 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', 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; return app;

View File

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

View File

@ -6,21 +6,22 @@ define(function(require) {
var ENCRYPTION_METHOD_TLS = 2; var ENCRYPTION_METHOD_TLS = 2;
var appCtrl = require('js/app-controller'), var appCtrl = require('js/app-controller'),
config = require('js/app-config').config, config = require('js/app-config').config;
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient');
var SetCredentialsCtrl = function($scope, $location, $routeParams) { var SetCredentialsCtrl = function($scope, $location, $routeParams) {
if (!appCtrl._emailDao && !$routeParams.dev) { if (!appCtrl._auth && !$routeParams.dev) {
$location.path('/'); // init app $location.path('/'); // init app
return; return;
} }
var auth = appCtrl._auth; 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.hasProviderPreset = !!config[provider];
$scope.useOAuth = !!auth.oauthToken; $scope.useOAuth = !!auth.oauthToken;
$scope.showDetails = (provider === 'custom'); $scope.showDetails = (provider === 'custom');
@ -30,12 +31,15 @@ define(function(require) {
} }
if ($scope.hasProviderPreset) { 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.smtpHost = config[provider].smtp.host;
$scope.smtpPort = config[provider].smtp.port; $scope.smtpPort = config[provider].smtp.port;
$scope.smtpCert = config[provider].smtp.ca; $scope.smtpCert = config[provider].smtp.ca;
$scope.smtpPinned = config[provider].smtp.pinned; $scope.smtpPinned = config[provider].smtp.pinned;
// transport encryption method
if (config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) { if (config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) {
$scope.smtpEncryption = ENCRYPTION_METHOD_TLS; $scope.smtpEncryption = ENCRYPTION_METHOD_TLS;
} else if (!config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) { } else if (!config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) {
@ -44,11 +48,13 @@ define(function(require) {
$scope.smtpEncryption = ENCRYPTION_METHOD_NONE; $scope.smtpEncryption = ENCRYPTION_METHOD_NONE;
} }
// IMAP config
$scope.imapHost = config[provider].imap.host; $scope.imapHost = config[provider].imap.host;
$scope.imapPort = config[provider].imap.port; $scope.imapPort = config[provider].imap.port;
$scope.imapCert = config[provider].imap.ca; $scope.imapCert = config[provider].imap.ca;
$scope.imapPinned = config[provider].imap.pinned; $scope.imapPinned = config[provider].imap.pinned;
// transport encryption method
if (config[provider].imap.secure && !config[provider].imap.ignoreTLS) { if (config[provider].imap.secure && !config[provider].imap.ignoreTLS) {
$scope.imapEncryption = ENCRYPTION_METHOD_TLS; $scope.imapEncryption = ENCRYPTION_METHOD_TLS;
} else if (!config[provider].imap.secure && !config[provider].imap.ignoreTLS) { } else if (!config[provider].imap.secure && !config[provider].imap.ignoreTLS) {
@ -58,109 +64,19 @@ define(function(require) {
} }
} }
$scope.test = function(imapClient, smtpClient) { $scope.test = function() {
var imapEncryption = parseInt($scope.imapEncryption, 10); // parse the <select> dropdown lists
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() {
var imapEncryption = parseInt($scope.imapEncryption, 10); var imapEncryption = parseInt($scope.imapEncryption, 10);
var smtpEncryption = parseInt($scope.smtpEncryption, 10); var smtpEncryption = parseInt($scope.smtpEncryption, 10);
auth.setCredentials({ // build credentials object
var credentials = {
provider: provider, provider: provider,
emailAddress: $scope.emailAddress, emailAddress: $scope.emailAddress,
username: $scope.username || $scope.emailAddress, username: $scope.username || $scope.emailAddress,
realname: $scope.realname, realname: $scope.realname,
password: $scope.password, password: $scope.password,
xoauth2: auth.oauthToken,
imap: { imap: {
host: $scope.imapHost.toLowerCase(), host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort, port: $scope.imapPort,
@ -177,13 +93,27 @@ define(function(require) {
ca: $scope.smtpCert, ca: $scope.smtpCert,
pinned: !!$scope.smtpPinned pinned: !!$scope.smtpPinned
} }
}); };
redirect();
}
function redirect() { // 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'); $location.path('/login');
} }
$scope.busy = false;
$scope.$apply();
});
};
}; };
return SetCredentialsCtrl; 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

@ -11,7 +11,15 @@
<form name="form"> <form name="form">
<div> <div>
<label class="input-error-message" ng-show="connectionError">Connection failed. Please check your credentials!</label> <div ng-show="connectionError">
<p class="input-error-message">{{connectionError.message}}</p>
<div ng-show="connectionError.underlyingError">
<a href="#" class="input-error-message" wo-touch='showUnderlyingError = !showUnderlyingError; $event.preventDefault()'>{{showUnderlyingError ? "Hide Details" : "Show Details"}}</a>
<p ng-show="showUnderlyingError" class="input-error-message">{{connectionError.underlyingError.message}}</p>
</div>
<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>
</div>
<label class="input-error-message" ng-show="form.$invalid || credentialsIncomplete">Please fill out all required fields!</label> <label class="input-error-message" ng-show="form.$invalid || credentialsIncomplete">Please fill out all required fields!</label>
<br> <br>
<input class="input-text" type="email" required ng-model="emailAddress" placeholder="Email address" focus-me="true" tabindex="1" spellcheck="false"></input> <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, var expect = chai.expect,
angular = require('angular'), angular = require('angular'),
mocks = require('angularMocks'), mocks = require('angularMocks'),
ImapClient = require('imap-client'), Auth = require('js/bo/auth'),
SmtpClient = require('smtpclient'), ConnectionDoctor = require('js/util/connection-doctor'),
SetCredentialsCtrl = require('js/controller/login-set-credentials'), SetCredentialsCtrl = require('js/controller/login-set-credentials'),
appController = require('js/app-controller'); appController = require('js/app-controller');
describe('Login (Set Credentials) Controller unit test', function() { describe('Login (Set Credentials) Controller unit test', function() {
var scope, location, setCredentialsCtrl; // Angular parameters
var imap, smtp; var scope, location, provider;
var origAuth;
var provider = 'providerproviderprovider'; // Stubs
var auth, origAuth, doctor, origDoctor;
// SUT
var setCredentialsCtrl;
beforeEach(function() { beforeEach(function() {
// remeber pre-test state to restore later
origAuth = appController._auth; origAuth = appController._auth;
appController._auth = {}; origDoctor = appController._doctor;
auth = appController._auth = sinon.createStubInstance(Auth);
imap = sinon.createStubInstance(ImapClient); doctor = appController._doctor = sinon.createStubInstance(ConnectionDoctor);
smtp = sinon.createStubInstance(SmtpClient);
// setup the controller
angular.module('setcredentialstest', []); angular.module('setcredentialstest', []);
mocks.module('setcredentialstest'); mocks.module('setcredentialstest');
mocks.inject(function($rootScope, $controller, $location) { mocks.inject(function($rootScope, $controller, $location) {
@ -40,14 +45,13 @@ define(function(require) {
}); });
afterEach(function() { afterEach(function() {
// restore pre-test state
appController._auth = origAuth; appController._auth = origAuth;
appController._doctor = origDoctor;
}); });
describe('set credentials', function() { describe('set credentials', function() {
it('should work', function(done) { it('should work', function() {
var imapCert = 'imapcertimapcertimapcertimapcertimapcertimapcert',
smtpCert = 'smtpcertsmtpcertsmtpcertsmtpcertsmtpcertsmtpcert';
scope.emailAddress = 'emailemailemailemail'; scope.emailAddress = 'emailemailemailemail';
scope.password = 'passwdpasswdpasswdpasswd'; scope.password = 'passwdpasswdpasswdpasswd';
scope.smtpHost = 'hosthosthost'; scope.smtpHost = 'hosthosthost';
@ -58,24 +62,19 @@ define(function(require) {
scope.imapEncryption = '2'; // TLS scope.imapEncryption = '2'; // TLS
scope.realname = 'peter pan'; scope.realname = 'peter pan';
imap.login.yields(); var expectedCredentials = {
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, provider: provider,
emailAddress: scope.emailAddress, emailAddress: scope.emailAddress,
username: scope.username || scope.emailAddress, username: scope.username || scope.emailAddress,
realname: scope.realname, realname: scope.realname,
password: scope.password, password: scope.password,
xoauth2: undefined,
imap: { imap: {
host: scope.imapHost.toLowerCase(), host: scope.imapHost.toLowerCase(),
port: scope.imapPort, port: scope.imapPort,
secure: true, secure: true,
ignoreTLS: false, ignoreTLS: false,
ca: scope.imapCert, ca: undefined,
pinned: false pinned: false
}, },
smtp: { smtp: {
@ -83,19 +82,19 @@ define(function(require) {
port: scope.smtpPort, port: scope.smtpPort,
secure: false, secure: false,
ignoreTLS: false, ignoreTLS: false,
ca: scope.smtpCert, ca: undefined,
pinned: false pinned: false
} }
});
done();
}; };
scope.test(imap, smtp); doctor.check.yields(); // synchronous yields!
imap.onCert(imapCert); scope.test();
smtp.oncert(smtpCert);
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/write-ctrl-test',
'test/unit/outbox-bo-test', 'test/unit/outbox-bo-test',
'test/unit/invitation-dao-test', 'test/unit/invitation-dao-test',
'test/unit/update-handler-test' 'test/unit/update-handler-test',
'test/unit/connection-doctor-test'
], function() { ], function() {
//Tests loaded, run tests //Tests loaded, run tests
mocha.run(); mocha.run();