Fix unit tests.

This commit is contained in:
Clement Michaud 2019-02-22 10:27:54 +01:00
parent 50d4ab1368
commit 5614bea827
29 changed files with 551 additions and 1128 deletions

View File

@ -1,49 +0,0 @@
module.exports = function (grunt) {
const buildDir = "dist";
const schemaDir = "server/src/lib/configuration/Configuration.schema.json"
grunt.initConfig({
clean: {
dist: ['dist'],
},
run: {
"test-server-unit": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'server/src/**/*.spec.ts']
},
"test-shared-unit": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'shared/**/*.spec.ts']
},
"test-cucumber": {
cmd: "./scripts/run-cucumber.sh",
args: ["./test/features"]
},
"test-complete-config": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'test/complete-config/**/*.ts']
},
"test-minimal-config": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'test/minimal-config/**/*.ts']
},
"test-inactivity": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'test/inactivity/**/*.ts']
},
"apidoc": {
cmd: "./node_modules/.bin/apidoc",
args: ["-i", "src/server", "-o", "doc"]
},
},
});
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-run');
grunt.registerTask('test-server', ['run:test-server-unit'])
grunt.registerTask('test-shared', ['run:test-shared-unit'])
grunt.registerTask('test-unit', ['test-server', 'test-shared']);
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']);
};

View File

@ -7,6 +7,7 @@ import CircleLoader, { Status } from "../CircleLoader/CircleLoader";
export interface OwnProps {
username: string;
redirectionUrl: string;
}
export interface DispatchProps {
@ -26,6 +27,7 @@ class AlreadyAuthenticated extends Component<Props> {
</div>
<div className={styles.statusIcon}><CircleLoader status={Status.SUCCESSFUL} /></div>
</div>
<a href={this.props.redirectionUrl}>{this.props.redirectionUrl}</a>
<div className={styles.logoutButtonContainer}>
<Button
onClick={this.props.onLogoutClicked}

View File

@ -39,7 +39,8 @@ class AuthenticationView extends Component<Props> {
redirectionUrl={this.props.redirectionUrl} />;
} else if (this.props.stage === Stage.ALREADY_AUTHENTICATED) {
return <AlreadyAuthenticated
username={this.props.remoteState.username}/>;
username={this.props.remoteState.username}
redirectionUrl={this.props.remoteState.default_redirection_url} />;
}
return <FirstFactorForm redirectionUrl={this.props.redirectionUrl} />;
}

View File

@ -3,6 +3,7 @@ import AuthenticationLevel from '../../types/AuthenticationLevel';
interface RemoteState {
username: string;
authentication_level: AuthenticationLevel;
default_redirection_url: string;
}
export default RemoteState;

1410
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
"scripts": {
"start": "./scripts/authelia-scripts start",
"build": "./scripts/authelia-scripts build",
"unittest": "./scripts/authelia-scripts unittest",
"test": "./scripts/authelia-scripts test",
"cover": "NODE_ENV=test nyc npm t"
},
@ -59,13 +60,12 @@
"@types/bluebird": "^3.5.4",
"@types/body-parser": "^1.16.3",
"@types/connect-redis": "0.0.7",
"@types/cucumber": "^4.0.1",
"@types/ejs": "^2.3.33",
"@types/express": "^4.0.35",
"@types/express-session": "1.15.8",
"@types/helmet": "0.0.37",
"@types/ldapjs": "^1.0.3",
"@types/mocha": "^5.0.0",
"@types/mocha": "^5.2.6",
"@types/mockdate": "^2.0.0",
"@types/mongodb": "^3.0.9",
"@types/nedb": "^1.8.3",
@ -90,7 +90,6 @@
"chromedriver": "^2.37.0",
"commander": "^2.19.0",
"concurrently": "^4.1.0",
"cucumber": "^4.0.0",
"grunt": "^1.0.3",
"grunt-contrib-clean": "^2.0.0",
"grunt-contrib-copy": "^1.0.0",
@ -101,7 +100,7 @@
"mockdate": "^2.0.1",
"node-fetch": "^2.3.0",
"nodemon": "^1.18.9",
"nyc": "^13.1.0",
"nyc": "^13.3.0",
"query-string": "^6.0.0",
"readable-stream": "^2.3.3",
"request": "^2.88.0",

View File

@ -9,7 +9,8 @@ program
.command('build', 'Build production version of Authelia from source.')
.command('clean', 'Clean the production version of Authelia.')
.command('serve', 'Serve production version of Authelia.')
.command('test', 'Run Authelia unit tests.')
.command('test', 'Run Authelia integration tests.')
.command('unittest', 'Run Authelia integration tests.')
.command('build-docker', 'Build Docker image containing production version of Authelia.')
.command('publish-docker', 'Publish Docker image containing production version of Authelia to Dockerhub.')

View File

@ -16,9 +16,10 @@ if (!program.suite) {
const ENVIRONMENT_FILENAME = '.suite';
const AUTHELIA_INTERRUPT_FILENAME = '.authelia-interrupt';
const CONFIG_FILEPATH = `test/suites/${program.suite}/config.yml`;
var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules', AUTHELIA_INTERRUPT_FILENAME], {
var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules', AUTHELIA_INTERRUPT_FILENAME, CONFIG_FILEPATH], {
persistent: true,
ignoreInitial: true,
});
@ -46,7 +47,7 @@ function startServer() {
}
exec('./node_modules/.bin/tslint', ['-c', 'server/tslint.json', '-p', 'server/tsconfig.json'])
.then(function() {
serverProcess = spawn('./scripts/run-dev-server.sh', [`test/suites/${program.suite}/config.yml`]);
serverProcess = spawn('./scripts/run-dev-server.sh', [CONFIG_FILEPATH]);
serverProcess.stdout.pipe(process.stdout);
serverProcess.stderr.pipe(process.stderr);
});

View File

@ -25,9 +25,9 @@ else if (program.args.length > 0) {
args = program.args;
// Render the production version of the nginx portal configuration
// execSync('./example/compose/nginx/portal/render.js --production');
execSync('./example/compose/nginx/portal/render.js --production');
// Prepare the environment
// execSync('./scripts/utils/prepare-environment.sh');
execSync('./scripts/utils/prepare-environment.sh');
}
else {
console.log('No suite detected but no tests have been selected...');

View File

@ -0,0 +1,40 @@
#!/usr/bin/env node
var program = require('commander');
var spawn = require('child_process').spawn;
program
.parse(process.argv);
async function runTests(pattern) {
return new Promise((resolve, reject) => {
mocha = spawn('./node_modules/.bin/mocha', ['--colors', '--require', 'ts-node/register', pattern], {
env: {
...process.env,
'TS_NODE_PROJECT': './server/tsconfig.json'
}
});
mocha.stdout.pipe(process.stdout);
mocha.stderr.pipe(process.stderr);
mocha.on('exit', (status) => {
if (status == 0) {
resolve();
}
reject(new Error('Status code ' + status));
});
});
}
async function test() {
await runTests('server/src/**/*.spec.ts');
await runTests('shared/**/*.spec.ts');
}
test()
.then(() => {
process.exit(0);
}, (err) => {
process.exit(1);
});

View File

@ -1,3 +0,0 @@
#!/bin/bash
WITH_SERVER=n TS_NODE_PROJECT=test/tsconfig.json ./node_modules/.bin/mocha --exit --colors --require ts-node/register $*

View File

@ -1,5 +0,0 @@
#!/bin/bash
REQ=`for f in test/features/step_definitions/*.ts; do echo "--require $f"; done;`
./node_modules/.bin/cucumber-js --format-options '{"colorsEnabled": true}' --require-module ts-node/register --require test/features/support/world.ts $REQ $*

View File

@ -1,3 +1,6 @@
#!/bin/sh
# Starts the server with the provided configuration in $1
# This scripts is called from authelia-scripts.
./node_modules/.bin/ts-node -P ./server/tsconfig.json ./server/src/index.ts $*

View File

@ -10,13 +10,13 @@ echo "node `node -v`"
echo "npm `npm -v`"
# Run unit tests
authelia-scripts test
authelia-scripts unittest
# Run integration tests
authelia-scripts test --headless test/suites/**/*.ts
# Build
authelia-scripts build
# Run integration/example tests
./scripts/integration-tests.sh
# Test npm deployment before actual deployment
# ./scripts/npm-deployment-test.sh

View File

@ -1,6 +1,6 @@
import sinon = require("sinon");
import IdentityValidator = require("./IdentityCheckMiddleware");
import * as IdentityCheckMiddleware from "./IdentityCheckMiddleware";
import exceptions = require("./Exceptions");
import { ServerVariables } from "./ServerVariables";
import Assert = require("assert");
@ -10,6 +10,7 @@ import ExpressMock = require("./stubs/express.spec");
import { IdentityValidableStub } from "./IdentityValidableStub.spec";
import { ServerVariablesMock, ServerVariablesMockBuilder }
from "./ServerVariablesMockBuilder.spec";
import { OPERATION_FAILED } from "../../../shared/UserMessages";
describe("IdentityCheckMiddleware", function () {
let req: ExpressMock.RequestMock;
@ -60,8 +61,8 @@ throws a first factor error", function () {
identityValidable.preValidationInitStub.returns(BluebirdPromise.reject(
new exceptions.FirstFactorValidationError(
"Error during prevalidation")));
const callback = IdentityValidator.get_start_validation(
identityValidable, "/endpoint", vars);
const callback = IdentityCheckMiddleware.post_start_validation(
identityValidable, vars);
return callback(req as any, res as any, undefined)
.then(() => {
@ -75,8 +76,8 @@ throws a first factor error", function () {
identityValidable.preValidationInitStub
.returns(BluebirdPromise.resolve(identity));
const callback = IdentityValidator
.get_start_validation(identityValidable, "/endpoint", vars);
const callback = IdentityCheckMiddleware
.post_start_validation(identityValidable, vars);
return callback(req as any, res as any, undefined)
.then(function () {
@ -87,13 +88,12 @@ throws a first factor error", function () {
// In that case we answer with 200 to avoid user enumeration.
it("should send 200 if userid is missing in provided identity",
function () {
const endpoint = "/protected";
const identity = { email: "abc@example.com" };
identityValidable.preValidationInitStub
.returns(BluebirdPromise.resolve(identity));
const callback = IdentityValidator
.get_start_validation(identityValidable, "/endpoint", vars);
const callback = IdentityCheckMiddleware
.post_start_validation(identityValidable, vars);
return callback(req as any, res as any, undefined)
.then(function () {
@ -101,52 +101,49 @@ throws a first factor error", function () {
});
});
it("should issue a token, send an email and return 204", function () {
const endpoint = "/protected";
it("should issue a token, send an email and return 204", async function () {
const identity = { userid: "user", email: "abc@example.com" };
req.get = sinon.stub().withArgs("Host").returns("localhost");
identityValidable.preValidationInitStub
.returns(BluebirdPromise.resolve(identity));
const callback = IdentityValidator
.get_start_validation(identityValidable, "/finish_endpoint", vars);
const callback = IdentityCheckMiddleware
.post_start_validation(identityValidable, vars);
return callback(req as any, res as any, undefined)
.then(function () {
Assert(mocks.notifier.notifyStub.calledOnce);
Assert(mocks.userDataStore.produceIdentityValidationTokenStub
.calledOnce);
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
.getCall(0).args[0], "user");
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
.getCall(0).args[3], 240000);
});
await callback(req as any, res as any, undefined)
Assert(mocks.notifier.notifyStub.calledOnce);
Assert(mocks.userDataStore.produceIdentityValidationTokenStub
.calledOnce);
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
.getCall(0).args[0], "user");
Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub
.getCall(0).args[3], 240000);
});
});
describe("test finish GET", function () {
it("should send 401 if no identity_token is provided", () => {
const callback = IdentityValidator
.get_finish_validation(identityValidable, vars);
it("should return an error if no identity_token is provided", () => {
const callback = IdentityCheckMiddleware
.post_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined)
.then(function () {
Assert(res.redirect.calledWith("/error/401"));
Assert(res.status.calledWith(200));
Assert(res.send.calledWith({'error': OPERATION_FAILED}));
});
});
it("should call postValidation if identity_token is provided and still \
valid", function () {
req.query.identity_token = "token";
const callback = IdentityValidator
.get_finish_validation(identityValidable, vars);
const callback = IdentityCheckMiddleware
.post_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined);
});
it("should return 401 if identity_token is provided but invalid",
it("should return an error if identity_token is provided but invalid",
function () {
req.query.identity_token = "token";
@ -156,11 +153,12 @@ valid", function () {
mocks.userDataStore.consumeIdentityValidationTokenStub
.returns(BluebirdPromise.reject(new Error("Invalid token")));
const callback = IdentityValidator
.get_finish_validation(identityValidable, vars);
const callback = IdentityCheckMiddleware
.post_finish_validation(identityValidable, vars);
return callback(req as any, res as any, undefined)
.then(() => {
Assert(res.redirect.calledWith("/error/401"));
Assert(res.status.calledWith(200));
Assert(res.send.calledWith({'error': OPERATION_FAILED}));
});
});
});

View File

@ -110,7 +110,7 @@ export function post_start_validation(handler: IdentityValidable,
return createAndSaveToken(userid, handler.challenge(),
vars.userDataStore);
})
.then((token) => {
.then((token: string) => {
const host = req.get("Host");
const link_url = util.format("https://%s%s?token=%s", host,
handler.destinationPath(), token);
@ -124,6 +124,7 @@ export function post_start_validation(handler: IdentityValidable,
return BluebirdPromise.resolve();
})
.catch(Exceptions.IdentityError, (err: Error) => {
vars.logger.error(req, err.message);
handler.preValidationResponse(req, res);
return BluebirdPromise.resolve();
})

View File

@ -24,6 +24,7 @@ export class IdentityValidableStub implements IdentityValidable {
this.postValidationResponseStub = Sinon.stub();
this.mailSubjectStub = Sinon.stub();
this.destinationPathStub = Sinon.stub();
}
challenge(): string {

View File

@ -6,11 +6,10 @@ import FirstFactorPost = require("./post");
import exceptions = require("../../Exceptions");
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
import Endpoints = require("../../../../../shared/api");
import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec");
import ExpressMock = require("../../stubs/express.spec");
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec";
import { ServerVariables } from "../../ServerVariables";
import AuthenticationError from "../../authentication/AuthenticationError";
describe("routes/firstfactor/post", function () {
let req: ExpressMock.RequestMock;
@ -73,7 +72,7 @@ describe("routes/firstfactor/post", function () {
emails: emails,
groups: groups
}));
req.body.keepMeLoggedIn = "true";
req.body.keepMeLoggedIn = true;
return FirstFactorPost.default(vars)(req as any, res as any);
});
@ -108,7 +107,7 @@ describe("routes/firstfactor/post", function () {
it("should return error message when LDAP authenticator throws", function () {
mocks.usersDatabase.checkUserPasswordStub.withArgs("username", "password")
.returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials")));
.returns(BluebirdPromise.reject(new AuthenticationError("Bad credentials")));
return FirstFactorPost.default(vars)(req as any, res as any)
.then(function () {

View File

@ -1,5 +1,3 @@
import Exceptions = require("../../Exceptions");
import * as ObjectPath from "object-path";
import BluebirdPromise = require("bluebird");
import express = require("express");
@ -98,8 +96,8 @@ export default function (vars: ServerVariables) {
})
.catch(AuthenticationError, function (err: Error) {
vars.regulator.mark(username, false);
return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.AUTHENTICATION_FAILED)(err);
return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED)(err);
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.AUTHENTICATION_FAILED));
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.OPERATION_FAILED));
};
}

View File

@ -1,10 +1,6 @@
import PasswordResetHandler
from "./PasswordResetHandler";
import { UserDataStore } from "../../../storage/UserDataStore";
import Sinon = require("sinon");
import winston = require("winston");
import assert = require("assert");
import BluebirdPromise = require("bluebird");
import ExpressMock = require("../../../stubs/express.spec");
import { ServerVariablesMock, ServerVariablesMockBuilder }
@ -13,15 +9,14 @@ import { ServerVariables } from "../../../ServerVariables";
describe("routes/password-reset/identity/PasswordResetHandler", function () {
let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock;
let mocks: ServerVariablesMock;
let vars: ServerVariables;
beforeEach(function () {
req = {
originalUrl: "/non-api/xxx",
query: {
userid: "user"
body: {
username: "user"
},
session: {
auth: {
@ -36,10 +31,6 @@ describe("routes/password-reset/identity/PasswordResetHandler", function () {
}
};
const options = {
inMemoryOnly: true
};
const s = ServerVariablesMockBuilder.build();
mocks = s.mocks;
vars = s.variables;
@ -52,12 +43,11 @@ describe("routes/password-reset/identity/PasswordResetHandler", function () {
.returns(BluebirdPromise.resolve({}));
mocks.userDataStore.consumeIdentityValidationTokenStub
.returns(BluebirdPromise.resolve({}));
res = ExpressMock.ResponseMock();
});
describe("test reset password identity pre check", () => {
it("should fail when no userid is provided", function () {
req.query.userid = undefined;
req.body.username = undefined;
const handler = new PasswordResetHandler(vars.logger,
vars.usersDatabase);
return handler.preValidationInit(req as any)

View File

@ -29,10 +29,10 @@ export default class PasswordResetHandler implements IdentityValidable {
return BluebirdPromise.resolve()
.then(function () {
that.logger.debug(req, "User '%s' requested a password reset", userid);
if (!userid)
if (!userid) {
return BluebirdPromise.reject(
new exceptions.AccessDeniedError("No user id provided"));
}
return that.usersDatabase.getEmails(userid);
})
.then(function (emails: string[]) {

View File

@ -34,7 +34,7 @@ export default function (vars: ServerVariables) {
return Bluebird.resolve();
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger,
UserMessages.AUTHENTICATION_TOTP_FAILED));
UserMessages.OPERATION_FAILED));
}
return handler;
}

View File

@ -36,10 +36,6 @@ describe("routes/secondfactor/u2f/register_request/get", function () {
mocks = s.mocks;
vars = s.variables;
const options = {
inMemoryOnly: true
};
mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({}));
mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({}));
@ -63,7 +59,6 @@ describe("routes/secondfactor/u2f/register_request/get", function () {
it("should return internal error on registration request", function () {
res.send = sinon.spy();
const user_key_container = {};
mocks.u2f.requestStub.returns(BluebirdPromise.reject("Internal error"));
return U2FRegisterRequestGet.default(vars)(req as any, res as any)
.then(function () {
@ -78,7 +73,10 @@ describe("routes/secondfactor/u2f/register_request/get", function () {
req.session.auth.identity_check = undefined;
return U2FRegisterRequestGet.default(vars)(req as any, res as any)
.then(function () {
Assert.equal(403, res.status.getCall(0).args[0]);
Assert.equal(200, res.status.getCall(0).args[0]);
Assert.deepEqual(res.send.getCall(0).args[0], {
error: "Operation failed."
});
});
});
});

View File

@ -4,12 +4,9 @@ import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import U2FSignPost = require("./post");
import { ServerVariables } from "../../../../ServerVariables";
import winston = require("winston");
import UserMessages = require("../../../../../../../shared/UserMessages");
import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../ServerVariablesMockBuilder.spec";
import ExpressMock = require("../../../../stubs/express.spec");
import U2FMock = require("../../../../stubs/u2f.spec");
import U2f = require("u2f");
import { Level } from "../../../../authentication/Level";
describe("routes/secondfactor/u2f/sign/post", function () {
@ -40,10 +37,6 @@ describe("routes/secondfactor/u2f/sign/post", function () {
req.headers = {};
req.headers.host = "localhost";
const options = {
inMemoryOnly: true
};
res = ExpressMock.ResponseMock();
res.send = sinon.spy();
res.json = sinon.spy();
@ -94,7 +87,7 @@ describe("routes/secondfactor/u2f/sign/post", function () {
.then(function () {
Assert.equal(res.status.getCall(0).args[0], 200);
Assert.deepEqual(res.send.getCall(0).args[0],
{ error: "Operation failed." });
{ error: UserMessages.OPERATION_FAILED });
});
});
});

View File

@ -1,4 +1,3 @@
import objectPath = require("object-path");
import u2f_common = require("../U2FCommon");
import BluebirdPromise = require("bluebird");
@ -46,7 +45,7 @@ export default function (vars: ServerVariables) {
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError200(req, res, vars.logger,
UserMessages.AUTHENTICATION_U2F_FAILED));
UserMessages.OPERATION_FAILED));
}
return handler;

View File

@ -9,7 +9,8 @@ export default function (vars: ServerVariables) {
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
res.json({
username: authSession.userid,
authentication_level: authSession.authentication_level
authentication_level: authSession.authentication_level,
default_redirection_url: vars.config.default_redirection_url,
});
resolve();
});

View File

@ -5,7 +5,7 @@ import VerifyForwardedHeaderIs from "../../../helpers/assertions/VerifyForwarded
import LoginOneFactor from "../../../helpers/behaviors/LoginOneFactor";
export default function() {
describe("Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend", function() {
describe.only("Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend", function() {
this.timeout(10000);
describe("With single factor", function() {

View File

@ -14,7 +14,7 @@ export default function() {
it("should be able to login after mongo restarts", async function() {
this.timeout(30000);
const secret = await LoginAndRegisterTotp(this.driver, "john", true);
const secret = await LoginAndRegisterTotp(this.driver, "john", "password", true);
child_process.execSync("./scripts/dc-dev.sh restart mongo");
await FullLogin(this.driver, "john", secret, "https://admin.example.com:8080/secret.html");
});

View File

@ -6,6 +6,8 @@ port: 9091
logs_level: debug
default_redirection_url: https://home.example.com:8080/
authentication_backend:
file:
path: ./users_database.yml