Notifications to users do not use notifyjs anymore. They are more common and located in the form areas to improve visibility on mobile devices.

pull/74/head
Clement Michaud 2017-08-04 21:20:31 +02:00
parent 0b8ac83566
commit 50636587a8
39 changed files with 425 additions and 245 deletions

View File

@ -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']);
};

View File

@ -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",

View File

@ -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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
<span>%s</span>', 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);
}
}

View File

@ -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<void> {
return new BluebirdPromise<void>(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."));
});
});
}

View File

@ -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.");
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<string> {
return new BluebirdPromise<string>(function (resolve, reject) {

View File

@ -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<void> {
return new BluebirdPromise<void>(function (resolve, reject) {
@ -22,11 +23,11 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQuerySta
});
}
function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> {
function startU2fAuthentication($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise<void> {
return new BluebirdPromise<void>(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<void> {
return startU2fAuthentication($, u2fApi);
export function validate($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise<void> {
return startU2fAuthentication($, notifier, u2fApi);
}

View File

@ -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;
}

View File

@ -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<string> {
const registrationData: U2f.RegistrationData = regResponse;
jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData));
return new BluebirdPromise<string>(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<string> {
return new BluebirdPromise<string>(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);
});
});
}

View File

@ -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;
}
});
});
}

View File

@ -5,6 +5,7 @@ block form-header
<img class="header-img" src="/img/user.png" alt="">
block content
<div class="notification"></div>
<form class="form-signin">
<div class="form-inputs">
<input type="text" class="form-control" id="username" placeholder="Username" required autofocus>

View File

@ -9,6 +9,7 @@ block form-header
<p>Set your new password and confirm it.</p>
block content
<div class="notification"></div>
<form class="form-signin">
<div class="form-inputs">
<input class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required" />

View File

@ -9,6 +9,7 @@ block form-header
<p>After giving your username, you will receive an email to change your password.</p>
block content
<div class="notification"></div>
<form class="form-signin">
<div class="form-inputs">
<input type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required" />

View File

@ -5,6 +5,7 @@ block form-header
<img class="header-img" src="../img/padlock.png" alt="">
block content
<div class="notification notification-totp"></div>
<form class="form-signin totp">
<div class="form-inputs">
<input type="text" class="form-control" id="token" placeholder="Token" required autofocus>
@ -14,6 +15,7 @@ block content
<span class="clearfix"></span>
</form>
<hr>
<div class="notification notification-u2f"></div>
<form class="form-signin u2f">
<button class="btn btn-lg btn-primary btn-block u2f-button" type="submit">U2F</button>
a(href=u2f_identity_start_endpoint, class="pull-right link register-u2f") Need to register?

View File

@ -1,4 +0,0 @@
interface JQueryStatic {
notify: any;
}

View File

@ -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/"

View File

@ -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"
Then I get a notification of type "warning" with message "The passwords are different."

View File

@ -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 |
| https://auth.test.local:8080/password-reset/identity/finish | 403 |

View File

@ -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)

View File

@ -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.");
});
});
});

View File

@ -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();

View File

@ -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");
});
});

View File

@ -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.");
});
});
});

View File

@ -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], "");
});
});
});

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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();

View File

@ -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;