diff --git a/Gruntfile.js b/Gruntfile.js index c5b590bb7..e499db587 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -162,7 +162,7 @@ module.exports = function (grunt) { grunt.registerTask('docker-restart', ['run:docker-restart']); grunt.registerTask('unit-tests', ['run:unit-tests']); - grunt.registerTask('integration-tests', ['run:unit-tests']); + grunt.registerTask('integration-tests', ['run:integration-tests']); grunt.registerTask('test', ['unit-tests']); }; diff --git a/package.json b/package.json index 4036504a0..60d720981 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ "jsdom": "^11.0.0", "mocha": "^3.4.2", "mockdate": "^2.0.1", - "notifyjs-browser": "^0.4.2", "nyc": "^10.3.2", "power-assert": "^1.4.4", "proxyquire": "^1.8.0", diff --git a/src/client/css/01-main.css b/src/client/css/01-main.css index 269b3eb0a..e4174f2c5 100644 --- a/src/client/css/01-main.css +++ b/src/client/css/01-main.css @@ -19,4 +19,43 @@ body { .poweredby { font-size: 0.7em; color: #6b6b6b; +} + +/* notifications */ + +.notification { + padding: 10px; + border-radius: 6px; + display: none; +} + +.notification img { + width: 24px; + margin-right: 10px; +} + +.notification i, +.notification span { + display:table-cell; + vertical-align:middle; +} + +.info { + border: 1px solid #9cb1ff; + background-color: rgb(192, 220, 255); +} + +.success { + border: 1px solid #65ec7c; + background-color: rgb(163, 255, 157); +} + +.error { + border: 1px solid #ffa3a3; + background-color: rgb(255, 175, 175); +} + +.warning { + border: 1px solid #ffd743; + background-color: rgb(255, 230, 143); } \ No newline at end of file diff --git a/src/client/index.ts b/src/client/index.ts index 8d7e37ce5..956edfc0b 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,12 +1,12 @@ -import FirstFactorValidator = require("./firstfactor/FirstFactorValidator"); +import FirstFactorValidator = require("./lib/firstfactor/FirstFactorValidator"); -import FirstFactor from "./firstfactor/index"; -import SecondFactor from "./secondfactor/index"; -import TOTPRegister from "./totp-register/totp-register"; -import U2fRegister from "./u2f-register/u2f-register"; -import ResetPasswordRequest from "./reset-password/reset-password-request"; -import ResetPasswordForm from "./reset-password/reset-password-form"; +import FirstFactor from "./lib/firstfactor/index"; +import SecondFactor from "./lib/secondfactor/index"; +import TOTPRegister from "./lib/totp-register/totp-register"; +import U2fRegister from "./lib/u2f-register/u2f-register"; +import ResetPasswordRequest from "./lib/reset-password/reset-password-request"; +import ResetPasswordForm from "./lib/reset-password/reset-password-form"; import jslogger = require("js-logger"); import jQuery = require("jquery"); import u2fApi = require("u2f-api"); @@ -14,8 +14,6 @@ import u2fApi = require("u2f-api"); jslogger.useDefaults(); jslogger.setLevel(jslogger.INFO); -require("notifyjs-browser")(jQuery); - export = { firstfactor: function () { FirstFactor(window, jQuery, FirstFactorValidator, jslogger); diff --git a/src/client/lib/INotifier.ts b/src/client/lib/INotifier.ts new file mode 100644 index 000000000..df947538f --- /dev/null +++ b/src/client/lib/INotifier.ts @@ -0,0 +1,14 @@ + +declare type Handler = () => void; + +export interface Handlers { + onFadedIn: Handler; + onFadedOut: Handler; +} + +export interface INotifier { + success(msg: string, handlers?: Handlers): void; + error(msg: string, handlers?: Handlers): void; + warning(msg: string, handlers?: Handlers): void; + info(msg: string, handlers?: Handlers): void; +} \ No newline at end of file diff --git a/src/client/lib/Notifier.ts b/src/client/lib/Notifier.ts new file mode 100644 index 000000000..cc59ee885 --- /dev/null +++ b/src/client/lib/Notifier.ts @@ -0,0 +1,45 @@ + + +import util = require("util"); +import { INotifier, Handlers } from "./INotifier"; + +export class Notifier implements INotifier { + private element: JQuery; + + constructor(selector: string, $: JQueryStatic) { + this.element = $(selector); + } + + private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void { + const that = this; + const FADE_TIME = 500; + const html = util.format('\ + %s', statusType, statusType, msg); + this.element.html(html); + this.element.addClass(statusType); + this.element.fadeIn(FADE_TIME, function() { + handlers.onFadedIn(); + }) + .delay(4000) + .fadeOut(FADE_TIME, function() { + that.element.removeClass(statusType); + handlers.onFadedOut(); + }); + } + + success(msg: string, handlers?: Handlers) { + this.displayAndFadeout(msg, "success", handlers); + } + + error(msg: string, handlers?: Handlers) { + this.displayAndFadeout(msg, "error", handlers); + } + + warning(msg: string, handlers?: Handlers) { + this.displayAndFadeout(msg, "warning", handlers); + } + + info(msg: string, handlers?: Handlers) { + this.displayAndFadeout(msg, "info", handlers); + } +} \ No newline at end of file diff --git a/src/client/firstfactor/FirstFactorValidator.ts b/src/client/lib/firstfactor/FirstFactorValidator.ts similarity index 62% rename from src/client/firstfactor/FirstFactorValidator.ts rename to src/client/lib/firstfactor/FirstFactorValidator.ts index 07a27f7d6..71ebc7ab4 100644 --- a/src/client/firstfactor/FirstFactorValidator.ts +++ b/src/client/lib/firstfactor/FirstFactorValidator.ts @@ -1,8 +1,8 @@ import BluebirdPromise = require("bluebird"); -import Endpoints = require("../../server/endpoints"); +import Endpoints = require("../../../server/endpoints"); -export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise < void> { +export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { $.post(Endpoints.FIRST_FACTOR_POST, { username: username, @@ -12,9 +12,7 @@ export function validate(username: string, password: string, $: JQueryStatic): B resolve(); }) .fail(function (xhr: JQueryXHR, textStatus: string) { - if (xhr.status == 401) - reject(new Error("Authetication failed. Please check your credentials.")); - reject(new Error(textStatus)); + reject(new Error("Authetication failed. Please check your credentials.")); }); }); } diff --git a/src/client/firstfactor/UISelectors.ts b/src/client/lib/firstfactor/UISelectors.ts similarity index 100% rename from src/client/firstfactor/UISelectors.ts rename to src/client/lib/firstfactor/UISelectors.ts diff --git a/src/client/firstfactor/index.ts b/src/client/lib/firstfactor/index.ts similarity index 73% rename from src/client/firstfactor/index.ts rename to src/client/lib/firstfactor/index.ts index fea6b4e31..23b4a40f8 100644 --- a/src/client/firstfactor/index.ts +++ b/src/client/lib/firstfactor/index.ts @@ -1,10 +1,15 @@ import FirstFactorValidator = require("./FirstFactorValidator"); import JSLogger = require("js-logger"); import UISelectors = require("./UISelectors"); +import { Notifier } from "../Notifier"; -import Endpoints = require("../../server/endpoints"); +import Endpoints = require("../../../server/endpoints"); + +export default function (window: Window, $: JQueryStatic, + firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) { + + const notifier = new Notifier(".notification", $); -export default function (window: Window, $: JQueryStatic, firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) { function onFormSubmitted() { const username: string = $(UISelectors.USERNAME_FIELD_ID).val(); const password: string = $(UISelectors.PASSWORD_FIELD_ID).val(); @@ -27,7 +32,7 @@ export default function (window: Window, $: JQueryStatic, firstFactorValidator: jslogger.debug("First factor failed."); $(UISelectors.PASSWORD_FIELD_ID).val(""); - $.notify("Error during authentication: " + err.message, "error"); + notifier.error("Authentication failed. Please double check your credentials."); } diff --git a/src/client/reset-password/constants.ts b/src/client/lib/reset-password/constants.ts similarity index 100% rename from src/client/reset-password/constants.ts rename to src/client/lib/reset-password/constants.ts diff --git a/src/client/reset-password/reset-password-form.ts b/src/client/lib/reset-password/reset-password-form.ts similarity index 69% rename from src/client/reset-password/reset-password-form.ts rename to src/client/lib/reset-password/reset-password-form.ts index dfd48e45d..26ec4ec1d 100644 --- a/src/client/reset-password/reset-password-form.ts +++ b/src/client/lib/reset-password/reset-password-form.ts @@ -1,9 +1,12 @@ import BluebirdPromise = require("bluebird"); -import Endpoints = require("../../server/endpoints"); +import Endpoints = require("../../../server/endpoints"); import Constants = require("./constants"); +import { Notifier } from "../Notifier"; export default function (window: Window, $: JQueryStatic) { + const notifier = new Notifier(".notification", $); + function modifyPassword(newPassword: string) { return new BluebirdPromise(function (resolve, reject) { $.post(Endpoints.RESET_PASSWORD_FORM_POST, { @@ -23,22 +26,22 @@ export default function (window: Window, $: JQueryStatic) { const password2 = $("#password2").val(); if (!password1 || !password2) { - $.notify("You must enter your new password twice.", "warn"); + notifier.warning("You must enter your new password twice."); return false; } if (password1 != password2) { - $.notify("The passwords are different", "warn"); + notifier.warning("The passwords are different."); return false; } modifyPassword(password1) .then(function () { - $.notify("Your password has been changed. Please login again", "success"); + notifier.success("Your password has been changed. Please log in again."); window.location.href = Endpoints.FIRST_FACTOR_GET; }) .error(function () { - $.notify("An error occurred during password change.", "warn"); + notifier.warning("An error occurred during password reset. Your password has not been changed."); }); return false; } diff --git a/src/client/reset-password/reset-password-request.ts b/src/client/lib/reset-password/reset-password-request.ts similarity index 73% rename from src/client/reset-password/reset-password-request.ts rename to src/client/lib/reset-password/reset-password-request.ts index 606309773..8ff450f40 100644 --- a/src/client/reset-password/reset-password-request.ts +++ b/src/client/lib/reset-password/reset-password-request.ts @@ -1,11 +1,14 @@ import BluebirdPromise = require("bluebird"); -import Endpoints = require("../../server/endpoints"); +import Endpoints = require("../../../server/endpoints"); import Constants = require("./constants"); import jslogger = require("js-logger"); +import { Notifier } from "../Notifier"; export default function(window: Window, $: JQueryStatic) { + const notifier = new Notifier(".notification", $); + function requestPasswordReset(username: string) { return new BluebirdPromise(function (resolve, reject) { $.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, { @@ -24,19 +27,19 @@ export default function(window: Window, $: JQueryStatic) { const username = $("#username").val(); if (!username) { - $.notify("You must provide your username to reset your password.", "warn"); + notifier.warning("You must provide your username to reset your password."); return; } requestPasswordReset(username) .then(function () { - $.notify("An email has been sent. Click on the link to change your password.", "success"); + notifier.success("An email has been sent to you. Follow the link to change your password."); setTimeout(function () { window.location.replace(Endpoints.FIRST_FACTOR_GET); }, 1000); }) .error(function () { - $.notify("Are you sure this is your username?", "warn"); + notifier.warning("Are you sure this is your username?"); }); return false; } diff --git a/src/client/secondfactor/TOTPValidator.ts b/src/client/lib/secondfactor/TOTPValidator.ts similarity index 91% rename from src/client/secondfactor/TOTPValidator.ts rename to src/client/lib/secondfactor/TOTPValidator.ts index 7538f7f1e..4b1d0ffbe 100644 --- a/src/client/secondfactor/TOTPValidator.ts +++ b/src/client/lib/secondfactor/TOTPValidator.ts @@ -1,6 +1,6 @@ import BluebirdPromise = require("bluebird"); -import Endpoints = require("../../server/endpoints"); +import Endpoints = require("../../../server/endpoints"); export function validate(token: string, $: JQueryStatic): BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { diff --git a/src/client/secondfactor/U2FValidator.ts b/src/client/lib/secondfactor/U2FValidator.ts similarity index 75% rename from src/client/secondfactor/U2FValidator.ts rename to src/client/lib/secondfactor/U2FValidator.ts index fb5da8e17..b1fdb595b 100644 --- a/src/client/secondfactor/U2FValidator.ts +++ b/src/client/lib/secondfactor/U2FValidator.ts @@ -2,8 +2,9 @@ import U2fApi = require("u2f-api"); import U2f = require("u2f"); import BluebirdPromise = require("bluebird"); -import { SignMessage } from "../../server/lib/routes/secondfactor/u2f/sign_request/SignMessage"; -import Endpoints = require("../../server/endpoints"); +import { SignMessage } from "../../../server/lib/routes/secondfactor/u2f/sign_request/SignMessage"; +import Endpoints = require("../../../server/endpoints"); +import { INotifier } from "../INotifier"; function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQueryStatic): BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { @@ -22,11 +23,11 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQuerySta }); } -function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise { +function startU2fAuthentication($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise { return new BluebirdPromise(function (resolve, reject) { $.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json") .done(function (signResponse: SignMessage) { - $.notify("Please touch the token", "info"); + notifier.info("Please touch the token"); const signRequest: U2fApi.SignRequest = { appId: signResponse.request.appId, @@ -41,7 +42,7 @@ function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): Bluebir .then(function (data) { resolve(data); }, function (err) { - $.notify("Error when finish U2F transaction", "error"); + notifier.error("Error when finish U2F transaction"); reject(err); }); }) @@ -56,6 +57,6 @@ function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): Bluebir } -export function validate($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise { - return startU2fAuthentication($, u2fApi); +export function validate($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise { + return startU2fAuthentication($, notifier, u2fApi); } diff --git a/src/client/secondfactor/constants.ts b/src/client/lib/secondfactor/constants.ts similarity index 100% rename from src/client/secondfactor/constants.ts rename to src/client/lib/secondfactor/constants.ts diff --git a/src/client/secondfactor/index.ts b/src/client/lib/secondfactor/index.ts similarity index 76% rename from src/client/secondfactor/index.ts rename to src/client/lib/secondfactor/index.ts index 1129bc2ae..022c4a95a 100644 --- a/src/client/secondfactor/index.ts +++ b/src/client/lib/secondfactor/index.ts @@ -4,24 +4,25 @@ import jslogger = require("js-logger"); import TOTPValidator = require("./TOTPValidator"); import U2FValidator = require("./U2FValidator"); - -import Endpoints = require("../../server/endpoints"); - +import Endpoints = require("../../../server/endpoints"); import Constants = require("./constants"); +import { Notifier } from "../Notifier"; export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) { + const notifierTotp = new Notifier(".notification-totp", $); + const notifierU2f = new Notifier(".notification-u2f", $); + function onAuthenticationSuccess(data: any) { window.location.href = data.redirection_url; } - function onSecondFactorTotpSuccess(data: any) { onAuthenticationSuccess(data); } function onSecondFactorTotpFailure(err: Error) { - $.notify("Error while validating TOTP token. Cause: " + err.message, "error"); + notifierTotp.error("Problem with TOTP validation."); } function onU2fAuthenticationSuccess(data: any) { @@ -29,10 +30,9 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) } function onU2fAuthenticationFailure() { - $.notify("Problem with U2F authentication. Did you register before authenticating?", "warn"); + notifierU2f.error("Problem with U2F validation. Did you register before authenticating?"); } - function onTOTPFormSubmitted(): boolean { const token = $(Constants.TOTP_TOKEN_SELECTOR).val(); jslogger.debug("TOTP token is %s", token); @@ -45,7 +45,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) function onU2FFormSubmitted(): boolean { jslogger.debug("Start U2F authentication"); - U2FValidator.validate($, U2fApi) + U2FValidator.validate($, notifierU2f, U2fApi) .then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure); return false; } diff --git a/src/client/totp-register/totp-register.ts b/src/client/lib/totp-register/totp-register.ts similarity index 100% rename from src/client/totp-register/totp-register.ts rename to src/client/lib/totp-register/totp-register.ts diff --git a/src/client/totp-register/ui-selector.ts b/src/client/lib/totp-register/ui-selector.ts similarity index 100% rename from src/client/totp-register/ui-selector.ts rename to src/client/lib/totp-register/ui-selector.ts diff --git a/src/client/lib/u2f-register/u2f-register.ts b/src/client/lib/u2f-register/u2f-register.ts new file mode 100644 index 000000000..e34706bc4 --- /dev/null +++ b/src/client/lib/u2f-register/u2f-register.ts @@ -0,0 +1,62 @@ + +import BluebirdPromise = require("bluebird"); +import U2f = require("u2f"); +import u2fApi = require("u2f-api"); +import Endpoints = require("../../../server/endpoints"); +import jslogger = require("js-logger"); +import { Notifier } from "../Notifier"; + +export default function (window: Window, $: JQueryStatic) { + const notifier = new Notifier(".notification", $); + + function checkRegistration(regResponse: u2fApi.RegisterResponse): BluebirdPromise { + const registrationData: U2f.RegistrationData = regResponse; + + jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData)); + + return new BluebirdPromise(function (resolve, reject) { + $.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json") + .done(function (data) { + resolve(data.redirection_url); + }) + .fail(function (xhr, status) { + reject(); + }); + }); + } + + function requestRegistration(): BluebirdPromise { + return new BluebirdPromise(function (resolve, reject) { + $.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, undefined, "json") + .done(function (registrationRequest: U2f.Request) { + jslogger.debug("registrationRequest = %s", JSON.stringify(registrationRequest)); + + const registerRequest: u2fApi.RegisterRequest = registrationRequest; + u2fApi.register([registerRequest], [], 120) + .then(function (res: u2fApi.RegisterResponse) { + return checkRegistration(res); + }) + .then(function (redirectionUrl: string) { + resolve(redirectionUrl); + }) + .catch(function (err: Error) { + reject(err); + }); + }); + }); + } + + function onRegisterFailure(err: Error) { + notifier.error("Problem while registering your U2F device."); + } + + $(document).ready(function () { + requestRegistration() + .then(function (redirectionUrl: string) { + document.location.href = redirectionUrl; + }) + .error(function (err) { + onRegisterFailure(err); + }); + }); +} diff --git a/src/client/u2f-register/u2f-register.ts b/src/client/u2f-register/u2f-register.ts deleted file mode 100644 index d584ab03b..000000000 --- a/src/client/u2f-register/u2f-register.ts +++ /dev/null @@ -1,53 +0,0 @@ - -import BluebirdPromise = require("bluebird"); -import U2f = require("u2f"); -import u2fApi = require("u2f-api"); - -import Endpoints = require("../../server/endpoints"); -import jslogger = require("js-logger"); - -export default function(window: Window, $: JQueryStatic) { - - function checkRegistration(regResponse: u2fApi.RegisterResponse, fn: (err: Error) => void) { - const registrationData: U2f.RegistrationData = regResponse; - - jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData)); - - $.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json") - .done(function (data) { - document.location.href = data.redirection_url; - }) - .fail(function (xhr, status) { - $.notify("Error when finish U2F transaction" + status); - }); - } - - function requestRegistration(fn: (err: Error) => void) { - $.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, undefined, "json") - .done(function (registrationRequest: U2f.Request) { - jslogger.debug("registrationRequest = %s", JSON.stringify(registrationRequest)); - - const registerRequest: u2fApi.RegisterRequest = registrationRequest; - u2fApi.register([registerRequest], [], 120) - .then(function (res: u2fApi.RegisterResponse) { - checkRegistration(res, fn); - }) - .catch(function (err: Error) { - fn(err); - }); - }); - } - - function onRegisterFailure(err: Error) { - $.notify("Problem authenticating with U2F.", "error"); - } - - $(document).ready(function () { - requestRegistration(function (err: Error) { - if (err) { - onRegisterFailure(err); - return; - } - }); - }); -} diff --git a/src/server/views/firstfactor.pug b/src/server/views/firstfactor.pug index ee8ed48a7..4c93b576b 100644 --- a/src/server/views/firstfactor.pug +++ b/src/server/views/firstfactor.pug @@ -5,6 +5,7 @@ block form-header block content + diff --git a/src/server/views/password-reset-form.pug b/src/server/views/password-reset-form.pug index e90c5e3fb..387edafd1 100644 --- a/src/server/views/password-reset-form.pug +++ b/src/server/views/password-reset-form.pug @@ -9,6 +9,7 @@ block form-header Set your new password and confirm it. block content + diff --git a/src/server/views/password-reset-request.pug b/src/server/views/password-reset-request.pug index 714ff0eac..ab3c2adc4 100644 --- a/src/server/views/password-reset-request.pug +++ b/src/server/views/password-reset-request.pug @@ -9,6 +9,7 @@ block form-header After giving your username, you will receive an email to change your password. block content + diff --git a/src/server/views/secondfactor.pug b/src/server/views/secondfactor.pug index 9ed8a7b53..cb9600fd6 100644 --- a/src/server/views/secondfactor.pug +++ b/src/server/views/secondfactor.pug @@ -5,6 +5,7 @@ block form-header block content + @@ -14,6 +15,7 @@ block content + U2F a(href=u2f_identity_start_endpoint, class="pull-right link register-u2f") Need to register? diff --git a/src/types/jquery-notify.d.ts b/src/types/jquery-notify.d.ts deleted file mode 100644 index 60d08cc11..000000000 --- a/src/types/jquery-notify.d.ts +++ /dev/null @@ -1,4 +0,0 @@ - -interface JQueryStatic { - notify: any; -} diff --git a/test/features/authentication.feature b/test/features/authentication.feature index b17c11c74..028bffeb9 100644 --- a/test/features/authentication.feature +++ b/test/features/authentication.feature @@ -12,7 +12,7 @@ Feature: User validate first factor When I set field "username" to "john" And I set field "password" to "bad-password" And I click on "Sign in" - Then I get a notification with message "Error during authentication: Authetication failed. Please check your credentials." + Then I get a notification of type "error" with message "Authentication failed. Please double check your credentials." Scenario: User succeeds TOTP second factor Given I visit "https://auth.test.local:8080/" @@ -29,7 +29,7 @@ Feature: User validate first factor And I login with user "john" and password "password" And I use "BADTOKEN" as TOTP token And I click on "TOTP" - Then I get a notification with message "Error while validating TOTP token. Cause: error" + Then I get a notification of type "error" with message "Problem with TOTP validation." Scenario: User logs out Given I visit "https://auth.test.local:8080/" diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature index b383840cf..c386f28c7 100644 --- a/test/features/reset-password.feature +++ b/test/features/reset-password.feature @@ -9,7 +9,7 @@ Feature: User is able to reset his password Given I'm on https://auth.test.local:8080/password-reset/request When I set field "username" to "james" And I click on "Reset Password" - Then I get a notification with message "An email has been sent. Click on the link to change your password." + Then I get a notification of type "success" with message "An email has been sent to you. Follow the link to change your password." Scenario: User resets his password Given I'm on https://auth.test.local:8080/password-reset/request @@ -30,4 +30,4 @@ Feature: User is able to reset his password And I set field "password1" to "newpassword" And I set field "password2" to "newpassword2" And I click on "Reset Password" - Then I get a notification with message "The passwords are different" \ No newline at end of file + Then I get a notification of type "warning" with message "The passwords are different." diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature index f87cd3ae8..e343d249a 100644 --- a/test/features/restrictions.feature +++ b/test/features/restrictions.feature @@ -13,5 +13,4 @@ Feature: Non authenticated users have no access to certain pages | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | | https://auth.test.local:8080/secondfactor/totp/identity/finish | 403 | | https://auth.test.local:8080/password-reset/identity/start | 403 | - | https://auth.test.local:8080/password-reset/identity/finish | 403 | - \ No newline at end of file + | https://auth.test.local:8080/password-reset/identity/finish | 403 | \ No newline at end of file diff --git a/test/features/step_definitions/authentication.ts b/test/features/step_definitions/authentication.ts index 853a3f640..35983f7e9 100644 --- a/test/features/step_definitions/authentication.ts +++ b/test/features/step_definitions/authentication.ts @@ -38,15 +38,6 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) { return this.useTotpTokenHandle(handle); }); - Then("I get a notification with message {stringInDoubleQuotes}", function (notificationMessage: string) { - const that = this; - that.driver.sleep(500); - return this.driver - .findElement(seleniumWebdriver.By.className("notifyjs-corner")) - .findElement(seleniumWebdriver.By.tagName("span")) - .findElement(seleniumWebdriver.By.xpath("//span[contains(.,'" + notificationMessage + "')]")); - }); - When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) { const that = this; return this.driver.get(url) diff --git a/test/features/step_definitions/notifications.ts b/test/features/step_definitions/notifications.ts new file mode 100644 index 000000000..9a13a64c7 --- /dev/null +++ b/test/features/step_definitions/notifications.ts @@ -0,0 +1,25 @@ +import Cucumber = require("cucumber"); +import seleniumWebdriver = require("selenium-webdriver"); +import Assert = require("assert"); +import Fs = require("fs"); +import CustomWorld = require("../support/world"); + +Cucumber.defineSupportCode(function ({ Given, When, Then }) { + Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}", + function (notificationType: string, notificationMessage: string) { + const that = this; + const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification")); + return this.driver.wait(seleniumWebdriver.until.elementIsVisible(notificationEl), 2000) + .then(function () { + return notificationEl.getText(); + }) + .then(function (txt: string) { + Assert.equal(notificationMessage, txt); + return notificationEl.getAttribute("class"); + }) + .then(function(classes: string) { + Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element."); + }); + }); + +}); \ No newline at end of file diff --git a/test/features/support/world.ts b/test/features/support/world.ts index 97bbf9f85..d456b14be 100644 --- a/test/features/support/world.ts +++ b/test/features/support/world.ts @@ -3,6 +3,7 @@ import seleniumWebdriver = require("selenium-webdriver"); import Cucumber = require("cucumber"); import Fs = require("fs"); import Speakeasy = require("speakeasy"); +import Assert = require("assert"); function CustomWorld() { const that = this; @@ -26,16 +27,26 @@ function CustomWorld() { }; this.getErrorPage = function (code: number) { - return this.driver - .findElement(seleniumWebdriver.By.tagName("h1")) - .findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]")); + const that = this; + return this.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.tagName("h1")), 2000) + .then(function () { + return that.driver + .findElement(seleniumWebdriver.By.tagName("h1")).getText(); + }) + .then(function (txt: string) { + Assert.equal(txt, "Error " + code); + }); }; this.clickOnButton = function (buttonText: string) { - return this.driver - .findElement(seleniumWebdriver.By.tagName("button")) - .findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]")) - .click(); + const that = this; + return this.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.tagName("button")), 2000) + .then(function () { + return that.driver + .findElement(seleniumWebdriver.By.tagName("button")) + .findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]")) + .click(); + }); }; this.loginWithUserPassword = function (username: string, password: string) { @@ -68,7 +79,7 @@ function CustomWorld() { return that.driver.get(link); }) .then(function () { - return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 1000); + return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 5000); }) .then(function () { return that.driver.findElement(seleniumWebdriver.By.id("secret")).getText(); diff --git a/test/unit/client/Notifier.test.ts b/test/unit/client/Notifier.test.ts new file mode 100644 index 000000000..17c8e246d --- /dev/null +++ b/test/unit/client/Notifier.test.ts @@ -0,0 +1,71 @@ + +import Assert = require("assert"); +import Sinon = require("sinon"); +import JQueryMock = require("./mocks/jquery"); + +import { Notifier } from "../../../src/client/lib/Notifier"; + +describe("test notifier", function() { + const SELECTOR = "dummy-selector"; + const MESSAGE = "This is a message"; + let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock }; + + beforeEach(function() { + jqueryMock = JQueryMock.JQueryMock(); + }); + + function should_fade_in_and_out_on_notification(notificationType: string): void { + const fadeInReturn = { + delay: Sinon.stub() + }; + + const delayReturn = { + fadeOut: Sinon.stub() + }; + + jqueryMock.element.fadeIn.returns(fadeInReturn); + jqueryMock.element.fadeIn.yields(); + delayReturn.fadeOut.yields(); + + fadeInReturn.delay.returns(delayReturn); + + function onFadedInCallback() { + Assert(jqueryMock.element.fadeIn.calledOnce); + Assert(jqueryMock.element.addClass.calledWith(notificationType)); + Assert(!jqueryMock.element.removeClass.calledWith(notificationType)); + } + + function onFadedOutCallback() { + Assert(jqueryMock.element.removeClass.calledWith(notificationType)); + } + + const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any); + + // Call the method by its name... Bad but allows code reuse. + (notifier as any)[notificationType](MESSAGE, { + onFadedIn: onFadedInCallback, + onFadedOut: onFadedOutCallback + }); + + Assert(jqueryMock.element.fadeIn.calledOnce); + Assert(fadeInReturn.delay.calledOnce); + Assert(delayReturn.fadeOut.calledOnce); + } + + + it("should fade in and fade out an error message", function() { + should_fade_in_and_out_on_notification("error"); + }); + + it("should fade in and fade out an info message", function() { + should_fade_in_and_out_on_notification("info"); + }); + + it("should fade in and fade out an warning message", function() { + should_fade_in_and_out_on_notification("warning"); + }); + + it("should fade in and fade out an success message", function() { + should_fade_in_and_out_on_notification("success"); + }); +}); \ No newline at end of file diff --git a/test/unit/client/firstfactor/FirstFactorValidator.test.ts b/test/unit/client/firstfactor/FirstFactorValidator.test.ts index 73a686dc2..5ce7533fc 100644 --- a/test/unit/client/firstfactor/FirstFactorValidator.test.ts +++ b/test/unit/client/firstfactor/FirstFactorValidator.test.ts @@ -1,5 +1,5 @@ -import FirstFactorValidator = require("../../../../src/client/firstfactor/FirstFactorValidator"); +import FirstFactorValidator = require("../../../../src/client/lib/firstfactor/FirstFactorValidator"); import JQueryMock = require("../mocks/jquery"); import BluebirdPromise = require("bluebird"); import Assert = require("assert"); @@ -11,23 +11,23 @@ describe("test FirstFactorValidator", function () { postPromise.done.returns(postPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.post.returns(postPromise); + jqueryMock.jquery.post.returns(postPromise); - return FirstFactorValidator.validate("username", "password", jqueryMock as any); + return FirstFactorValidator.validate("username", "password", jqueryMock.jquery as any); }); - function should_fail_first_factor_validation(statusCode: number, errorMessage: string) { + function should_fail_first_factor_validation(errorMessage: string) { const xhr = { - status: statusCode + status: 401 }; const postPromise = JQueryMock.JQueryDeferredMock(); postPromise.fail.yields(xhr, errorMessage); postPromise.done.returns(postPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.post.returns(postPromise); + jqueryMock.jquery.post.returns(postPromise); - return FirstFactorValidator.validate("username", "password", jqueryMock as any) + return FirstFactorValidator.validate("username", "password", jqueryMock.jquery as any) .then(function () { return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); }, function (err: Error) { @@ -37,12 +37,8 @@ describe("test FirstFactorValidator", function () { } describe("should fail first factor validation", () => { - it("should fail with error 500", () => { - return should_fail_first_factor_validation(500, "Internal error"); - }); - - it("should fail with error 401", () => { - return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials."); + it("should fail with error", () => { + return should_fail_first_factor_validation("Authetication failed. Please check your credentials."); }); }); }); \ No newline at end of file diff --git a/test/unit/client/firstfactor/login.test.ts b/test/unit/client/firstfactor/login.test.ts deleted file mode 100644 index daf9688c7..000000000 --- a/test/unit/client/firstfactor/login.test.ts +++ /dev/null @@ -1,87 +0,0 @@ - -import Endpoints = require("../../../../src/server/endpoints"); -import BluebirdPromise = require("bluebird"); - -import UISelectors = require("../../../../src/client/firstfactor/UISelectors"); -import firstfactor from "../../../../src/client/firstfactor/index"; -import JQueryMock = require("../mocks/jquery"); -import Assert = require("assert"); -import sinon = require("sinon"); -import jslogger = require("js-logger"); - -describe("test first factor page", () => { - it("should validate first factor", () => { - const jQuery = JQueryMock.JQueryMock(); - const window = { - location: { - search: "?redirect=https://example.com", - href: "" - }, - document: {}, - }; - - const thenSpy = sinon.spy(); - const FirstFactorValidator: any = { - validate: sinon.stub().returns({ then: thenSpy }) - }; - - firstfactor(window as Window, jQuery as any, FirstFactorValidator, jslogger); - const readyCallback = jQuery.getCall(0).returnValue.ready.getCall(0).args[0]; - readyCallback(); - - const onSubmitCallback = jQuery.getCall(1).returnValue.on.getCall(0).args[1]; - jQuery.onCall(2).returns({ val: sinon.stub() }); - jQuery.onCall(3).returns({ val: sinon.stub() }); - jQuery.onCall(4).returns({ val: sinon.stub() }); - jQuery.onCall(5).returns({ val: sinon.stub() }); - - onSubmitCallback(); - - const successCallback = thenSpy.getCall(0).args[0]; - successCallback(); - - Assert.equal(window.location.href, Endpoints.SECOND_FACTOR_GET); - }); - - describe("fail to validate first factor", () => { - let jQuery: JQueryMock.JQueryMock; - beforeEach(function () { - jQuery = JQueryMock.JQueryMock(); - const window = { - location: { - search: "?redirect=https://example.com", - href: "" - }, - document: {}, - }; - - const thenSpy = sinon.spy(); - const FirstFactorValidator: any = { - validate: sinon.stub().returns({ then: thenSpy }) - }; - - firstfactor(window as Window, jQuery as any, FirstFactorValidator, jslogger); - const readyCallback = jQuery.getCall(0).returnValue.ready.getCall(0).args[0]; - readyCallback(); - - const onSubmitCallback = jQuery.getCall(1).returnValue.on.getCall(0).args[1]; - jQuery.onCall(2).returns({ val: sinon.stub() }); - jQuery.onCall(3).returns({ val: sinon.stub() }); - jQuery.onCall(4).returns({ val: sinon.stub() }); - jQuery.onCall(5).returns({ val: sinon.stub() }); - - onSubmitCallback(); - - const failureCallback = thenSpy.getCall(0).args[1]; - failureCallback(new Error("Error when validating first factor")); - }); - - it("should notify the user there is a failure", function () { - Assert(jQuery.notify.calledOnce); - }); - - it("should reset the password field", function () { - Assert.equal(jQuery.getCall(4).returnValue.val.getCall(0).args[0], ""); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/client/mocks/NotifierStub.ts b/test/unit/client/mocks/NotifierStub.ts new file mode 100644 index 000000000..66cf37696 --- /dev/null +++ b/test/unit/client/mocks/NotifierStub.ts @@ -0,0 +1,33 @@ + +import Sinon = require("sinon"); +import { INotifier } from "../../../../src/client/lib/INotifier"; + +export class NotifierStub implements INotifier { + successStub: Sinon.SinonStub; + errorStub: Sinon.SinonStub; + warnStub: Sinon.SinonStub; + infoStub: Sinon.SinonStub; + + constructor() { + this.successStub = Sinon.stub(); + this.errorStub = Sinon.stub(); + this.warnStub = Sinon.stub(); + this.infoStub = Sinon.stub(); + } + + success(msg: string) { + this.successStub(); + } + + error(msg: string) { + this.errorStub(); + } + + warning(msg: string) { + this.warnStub(); + } + + info(msg: string) { + this.infoStub(); + } +} \ No newline at end of file diff --git a/test/unit/client/mocks/jquery.ts b/test/unit/client/mocks/jquery.ts index 905840ac6..e21f0b8ca 100644 --- a/test/unit/client/mocks/jquery.ts +++ b/test/unit/client/mocks/jquery.ts @@ -10,17 +10,32 @@ export interface JQueryMock extends sinon.SinonStub { notify: sinon.SinonStub; } +export interface JQueryElementsMock { + ready: sinon.SinonStub; + show: sinon.SinonStub; + hide: sinon.SinonStub; + html: sinon.SinonStub; + addClass: sinon.SinonStub; + removeClass: sinon.SinonStub; + fadeIn: sinon.SinonStub; + on: sinon.SinonStub; +} + export interface JQueryDeferredMock { done: sinon.SinonStub; fail: sinon.SinonStub; } -export function JQueryMock(): JQueryMock { +export function JQueryMock(): { jquery: JQueryMock, element: JQueryElementsMock } { const jquery = sinon.stub() as any; - const jqueryInstance = { + const jqueryInstance: JQueryElementsMock = { ready: sinon.stub(), show: sinon.stub(), hide: sinon.stub(), + html: sinon.stub(), + addClass: sinon.stub(), + removeClass: sinon.stub(), + fadeIn: sinon.stub(), on: sinon.stub() }; jquery.ajax = sinon.stub(); @@ -28,7 +43,10 @@ export function JQueryMock(): JQueryMock { jquery.post = sinon.stub(); jquery.notify = sinon.stub(); jquery.returns(jqueryInstance); - return jquery; + return { + jquery: jquery, + element: jqueryInstance + }; } export function JQueryDeferredMock(): JQueryDeferredMock { diff --git a/test/unit/client/secondfactor/TOTPValidator.test.ts b/test/unit/client/secondfactor/TOTPValidator.test.ts index 260249045..c2fa57d61 100644 --- a/test/unit/client/secondfactor/TOTPValidator.test.ts +++ b/test/unit/client/secondfactor/TOTPValidator.test.ts @@ -1,5 +1,5 @@ -import TOTPValidator = require("../../../../src/client/secondfactor/TOTPValidator"); +import TOTPValidator = require("../../../../src/client/lib/secondfactor/TOTPValidator"); import JQueryMock = require("../mocks/jquery"); import BluebirdPromise = require("bluebird"); import Assert = require("assert"); @@ -11,9 +11,9 @@ describe("test TOTPValidator", function () { postPromise.done.returns(postPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.ajax.returns(postPromise); + jqueryMock.jquery.ajax.returns(postPromise); - return TOTPValidator.validate("totp_token", jqueryMock as any); + return TOTPValidator.validate("totp_token", jqueryMock.jquery as any); }); it("should fail validating TOTP token", () => { @@ -24,9 +24,9 @@ describe("test TOTPValidator", function () { postPromise.done.returns(postPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.ajax.returns(postPromise); + jqueryMock.jquery.ajax.returns(postPromise); - return TOTPValidator.validate("totp_token", jqueryMock as any) + return TOTPValidator.validate("totp_token", jqueryMock.jquery as any) .then(function () { return BluebirdPromise.reject(new Error("Registration successfully finished while it should have not.")); }, function (err: Error) { diff --git a/test/unit/client/secondfactor/U2FValidator.test.ts b/test/unit/client/secondfactor/U2FValidator.test.ts index d9f72c320..c464715a4 100644 --- a/test/unit/client/secondfactor/U2FValidator.test.ts +++ b/test/unit/client/secondfactor/U2FValidator.test.ts @@ -1,12 +1,20 @@ -import U2FValidator = require("../../../../src/client/secondfactor/U2FValidator"); +import U2FValidator = require("../../../../src/client/lib/secondfactor/U2FValidator"); +import { INotifier } from "../../../../src/client/lib/INotifier"; import JQueryMock = require("../mocks/jquery"); import U2FApiMock = require("../mocks/u2f-api"); import { SignMessage } from "../../../../src/server/lib/routes/secondfactor/u2f/sign_request/SignMessage"; import BluebirdPromise = require("bluebird"); import Assert = require("assert"); +import { NotifierStub } from "../mocks/NotifierStub"; describe("test U2F validation", function () { + let notifier: INotifier; + + beforeEach(function() { + notifier = new NotifierStub(); + }); + it("should validate the U2F device", () => { const signatureRequest: SignMessage = { keyHandle: "keyhandle", @@ -28,10 +36,10 @@ describe("test U2F validation", function () { postPromise.done.returns(postPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.get.returns(getPromise); - jqueryMock.ajax.returns(postPromise); + jqueryMock.jquery.get.returns(getPromise); + jqueryMock.jquery.ajax.returns(postPromise); - return U2FValidator.validate(jqueryMock as any, u2fClient as any); + return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any); }); it("should fail during initial authentication request", () => { @@ -42,9 +50,9 @@ describe("test U2F validation", function () { getPromise.fail.yields(undefined, "Error while issuing authentication request"); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.get.returns(getPromise); + jqueryMock.jquery.get.returns(getPromise); - return U2FValidator.validate(jqueryMock as any, u2fClient as any) + return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any) .catch(function(err: Error) { Assert.equal("Error while issuing authentication request", err.message); return BluebirdPromise.resolve(); @@ -68,9 +76,9 @@ describe("test U2F validation", function () { getPromise.done.returns(getPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.get.returns(getPromise); + jqueryMock.jquery.get.returns(getPromise); - return U2FValidator.validate(jqueryMock as any, u2fClient as any) + return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any) .catch(function(err: Error) { Assert.equal("Device unable to sign", err.message); return BluebirdPromise.resolve(); @@ -98,10 +106,10 @@ describe("test U2F validation", function () { postPromise.done.returns(postPromise); const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.get.returns(getPromise); - jqueryMock.ajax.returns(postPromise); + jqueryMock.jquery.get.returns(getPromise); + jqueryMock.jquery.ajax.returns(postPromise); - return U2FValidator.validate(jqueryMock as any, u2fClient as any) + return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any) .catch(function(err: Error) { Assert.equal("Error while finishing authentication", err.message); return BluebirdPromise.resolve(); diff --git a/test/unit/client/totp-register/totp-register.test.ts b/test/unit/client/totp-register/totp-register.test.ts index 9d01ffe01..5e97a84e8 100644 --- a/test/unit/client/totp-register/totp-register.test.ts +++ b/test/unit/client/totp-register/totp-register.test.ts @@ -2,8 +2,8 @@ import sinon = require("sinon"); import assert = require("assert"); -import UISelector = require("../../../../src/client/totp-register/ui-selector"); -import TOTPRegister = require("../../../../src/client/totp-register/totp-register"); +import UISelector = require("../../../../src/client/lib/totp-register/ui-selector"); +import TOTPRegister = require("../../../../src/client/lib/totp-register/totp-register"); describe("test totp-register", function() { let jqueryMock: any;
Set your new password and confirm it.
After giving your username, you will receive an email to change your password.