Implement retry mechanism for broken connections to mongo (#258)

Before this patch, when Authelia started, if Mongo was not
up and running, Authelia failed to connect and never retried.
Now, everytime Authelia faces a broken connection, it tries
to reconnect during the next operation.
This commit is contained in:
Clément Michaud 2018-08-19 16:51:36 +02:00 committed by GitHub
parent 0dd9a5f815
commit c503765dd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 366 additions and 242 deletions

View File

@ -45,6 +45,10 @@ module.exports = function (grunt) {
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']
@ -187,7 +191,7 @@ module.exports = function (grunt) {
grunt.registerTask('test-server', ['env:env-test-server-unit', 'run:test-server-unit'])
grunt.registerTask('test-client', ['env:env-test-client-unit', 'run:test-client-unit'])
grunt.registerTask('test-unit', ['test-server', 'test-client']);
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config']);
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config']);
grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);
grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']);

View File

@ -48,7 +48,8 @@ export default class Server {
private setup(config: Configuration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> {
const that = this;
return ServerVariablesInitializer.initialize(config, this.requestLogger, deps)
return ServerVariablesInitializer.initialize(
config, this.globalLogger, this.requestLogger, deps)
.then(function (vars: ServerVariables) {
Configurator.configure(config, app, vars, deps);
return BluebirdPromise.resolve();

View File

@ -32,15 +32,16 @@ import { IAccessController } from "./access_control/IAccessController";
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
import { ICollectionFactory } from "./storage/ICollectionFactory";
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
import { MongoConnectorFactory } from "./connectors/mongo/MongoConnectorFactory";
import { IMongoClient } from "./connectors/mongo/IMongoClient";
import { GlobalDependencies } from "../../types/Dependencies";
import { ServerVariables } from "./ServerVariables";
import { MethodCalculator } from "./authentication/MethodCalculator";
import { MongoClient } from "./connectors/mongo/MongoClient";
import { IGlobalLogger } from "./logging/IGlobalLogger";
class UserDataStoreFactory {
static create(config: Configuration.Configuration): BluebirdPromise<UserDataStore> {
static create(config: Configuration.Configuration, globalLogger: IGlobalLogger): BluebirdPromise<UserDataStore> {
if (config.storage.local) {
const nedbOptions: Nedb.DataStoreOptions = {
filename: config.storage.local.path,
@ -50,13 +51,12 @@ class UserDataStoreFactory {
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
}
else if (config.storage.mongo) {
const mongoConnectorFactory = new MongoConnectorFactory();
const mongoConnector = mongoConnectorFactory.create(config.storage.mongo.url);
return mongoConnector.connect(config.storage.mongo.database)
.then(function (client: IMongoClient) {
const collectionFactory = CollectionFactoryFactory.createMongo(client);
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
});
const mongoClient = new MongoClient(
config.storage.mongo.url,
config.storage.mongo.database,
globalLogger);
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
return BluebirdPromise.resolve(new UserDataStore(collectionFactory));
}
return BluebirdPromise.reject(new Error("Storage backend incorrectly configured."));
@ -64,8 +64,12 @@ class UserDataStoreFactory {
}
export class ServerVariablesInitializer {
static initialize(config: Configuration.Configuration, requestLogger: IRequestLogger,
static initialize(
config: Configuration.Configuration,
globalLogger: IGlobalLogger,
requestLogger: IRequestLogger,
deps: GlobalDependencies): BluebirdPromise<ServerVariables> {
const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder);
const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs);
@ -78,7 +82,7 @@ export class ServerVariablesInitializer {
const accessController = new AccessController(config.access_control, deps.winston);
const totpHandler = new TotpHandler(deps.speakeasy);
return UserDataStoreFactory.create(config)
return UserDataStoreFactory.create(config, globalLogger)
.then(function (userDataStore: UserDataStore) {
const regulator = new Regulator(userDataStore, config.regulation.max_retries,
config.regulation.find_time, config.regulation.ban_time);

View File

@ -1,5 +1,6 @@
import MongoDB = require("mongodb");
import Bluebird = require("bluebird");
export interface IMongoClient {
collection(name: string): MongoDB.Collection;
collection(name: string): Bluebird<MongoDB.Collection>
}

View File

@ -1,7 +0,0 @@
import BluebirdPromise = require("bluebird");
import { IMongoClient } from "./IMongoClient";
export interface IMongoConnector {
connect(databaseName: string): BluebirdPromise<IMongoClient>;
close(): BluebirdPromise<void>;
}

View File

@ -1,5 +0,0 @@
import { IMongoConnector } from "./IMongoConnector";
export interface IMongoConnectorFactory {
create(url: string): IMongoConnector;
}

View File

@ -1,38 +1,72 @@
import Assert = require("assert");
import Sinon = require("sinon");
import MongoDB = require("mongodb");
import Bluebird = require("bluebird");
import { MongoClient } from "./MongoClient";
import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec";
describe("connectors/mongo/MongoClient", function () {
let mongoClientConnectStub: Sinon.SinonStub;
let mongoDatabase: any;
let mongoDatabaseCollectionStub: Sinon.SinonStub;
let MongoClientStub: any;
let mongoClientStub: any;
let mongoDatabaseStub: any;
let logger: GlobalLoggerStub = new GlobalLoggerStub();
describe("collection", function () {
before(function () {
mongoDatabaseCollectionStub = Sinon.stub();
mongoDatabase = {
collection: mongoDatabaseCollectionStub
before(function() {
mongoClientStub = {
db: Sinon.stub()
};
mongoClientConnectStub = Sinon.stub(MongoDB.MongoClient, "connect");
mongoClientConnectStub.yields(undefined, mongoDatabase);
mongoDatabaseStub = {
on: Sinon.stub(),
collection: Sinon.stub()
}
});
after(function () {
mongoClientConnectStub.restore();
describe("Connection to mongo is ok", function() {
before(function () {
MongoClientStub = Sinon.stub(
MongoDB.MongoClient, "connect");
MongoClientStub.yields(
undefined, mongoClientStub);
mongoClientStub.db.returns(
mongoDatabaseStub);
});
after(function () {
MongoClientStub.restore();
});
it("should create a collection", function () {
const COLLECTION_NAME = "mycollection";
const client = new MongoClient("mongo://url", "databasename", logger);
mongoDatabaseStub.collection.returns("COL");
return client.collection(COLLECTION_NAME)
.then((collection) => mongoDatabaseStub.collection.calledWith(COLLECTION_NAME));
});
});
it("should create a collection", function () {
const COLLECTION_NAME = "mycollection";
const client = new MongoClient(mongoDatabase);
describe("Connection to mongo is broken", function() {
before(function () {
MongoClientStub = Sinon.stub(
MongoDB.MongoClient, "connect");
MongoClientStub.yields(
new Error("Failed connection"), undefined);
});
after(function () {
MongoClientStub.restore();
});
mongoDatabaseCollectionStub.returns({});
const collection = client.collection(COLLECTION_NAME);
Assert(collection);
Assert(mongoDatabaseCollectionStub.calledWith(COLLECTION_NAME ));
});
it("should fail creating the collection", function() {
const COLLECTION_NAME = "mycollection";
const client = new MongoClient("mongo://url", "databasename", logger);
mongoDatabaseStub.collection.returns("COL");
return client.collection(COLLECTION_NAME)
.then((collection) => Bluebird.reject(new Error("should not be here")))
.error((err) => Bluebird.resolve());
});
})
});
});

View File

@ -1,15 +1,60 @@
import MongoDB = require("mongodb");
import { IMongoClient } from "./IMongoClient";
import Bluebird = require("bluebird");
import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages";
import { IGlobalLogger } from "../../logging/IGlobalLogger";
export class MongoClient implements IMongoClient {
private db: MongoDB.Db;
private url: string;
private databaseName: string;
constructor(db: MongoDB.Db) {
this.db = db;
private database: MongoDB.Db;
private client: MongoDB.MongoClient;
private logger: IGlobalLogger;
constructor(
url: string,
databaseName: string,
logger: IGlobalLogger) {
this.url = url;
this.databaseName = databaseName;
this.logger = logger;
}
collection(name: string): MongoDB.Collection {
return this.db.collection(name);
connect(): Bluebird<void> {
const that = this;
const connectAsync = Bluebird.promisify(MongoDB.MongoClient.connect);
return connectAsync(this.url)
.then(function (client: MongoDB.MongoClient) {
that.database = client.db(that.databaseName);
that.database.on("close", () => {
that.logger.info("[MongoClient] Lost connection.");
});
that.database.on("reconnect", () => {
that.logger.info("[MongoClient] Reconnected.");
});
that.client = client;
});
}
close(): Bluebird<void> {
if (this.client) {
this.client.close();
this.database = undefined;
this.client = undefined;
}
return Bluebird.resolve();
}
collection(name: string): Bluebird<MongoDB.Collection> {
if (!this.client) {
const that = this;
return this.connect()
.then(() => Bluebird.resolve(that.database.collection(name)));
}
return Bluebird.resolve(this.database.collection(name));
}
}

View File

@ -1,5 +1,6 @@
import Sinon = require("sinon");
import MongoDB = require("mongodb");
import Bluebird = require("bluebird");
import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient";
export class MongoClientStub implements IMongoClient {
@ -9,7 +10,7 @@ export class MongoClientStub implements IMongoClient {
this.collectionStub = Sinon.stub();
}
collection(name: string): MongoDB.Collection {
collection(name: string): Bluebird<MongoDB.Collection> {
return this.collectionStub(name);
}
}

View File

@ -1,47 +0,0 @@
import Assert = require("assert");
import Sinon = require("sinon");
import MongoDB = require("mongodb");
import BluebirdPromise = require("bluebird");
import { IMongoClient } from "./IMongoClient";
import { MongoConnector } from "./MongoConnector";
describe("connectors/mongo/MongoConnector", function () {
let mongoClientConnectStub: Sinon.SinonStub;
describe("create", function () {
before(function () {
mongoClientConnectStub = Sinon.stub(MongoDB.MongoClient, "connect");
});
after(function() {
mongoClientConnectStub.restore();
});
it("should create a connector", function () {
const client = { db: Sinon.mock() };
mongoClientConnectStub.yields(undefined, client);
const url = "mongodb://test.url";
const connector = new MongoConnector(url);
return connector.connect("database")
.then(function (client: IMongoClient) {
Assert(client);
Assert(mongoClientConnectStub.calledWith(url));
});
});
it("should fail creating a connector", function () {
mongoClientConnectStub.yields(new Error("Error while creating mongo client"));
const url = "mongodb://test.url";
const connector = new MongoConnector(url);
return connector.connect("database")
.then(function () { return BluebirdPromise.reject(new Error("It should not be here")); })
.error(function (client: IMongoClient) {
Assert(client);
Assert(mongoClientConnectStub.calledWith(url));
return BluebirdPromise.resolve();
});
});
});
});

View File

@ -1,31 +0,0 @@
import MongoDB = require("mongodb");
import BluebirdPromise = require("bluebird");
import { IMongoClient } from "./IMongoClient";
import { IMongoConnector } from "./IMongoConnector";
import { MongoClient } from "./MongoClient";
export class MongoConnector implements IMongoConnector {
private url: string;
private client: MongoDB.MongoClient;
constructor(url: string) {
this.url = url;
}
connect(databaseName: string): BluebirdPromise<IMongoClient> {
const that = this;
const connectAsync = BluebirdPromise.promisify(MongoDB.MongoClient.connect);
return connectAsync(this.url)
.then(function (client: MongoDB.MongoClient) {
that.client = client;
const db = client.db(databaseName);
return BluebirdPromise.resolve(new MongoClient(db));
});
}
close(): BluebirdPromise<void> {
this.client.close();
return BluebirdPromise.resolve();
}
}

View File

@ -1,13 +0,0 @@
import Assert = require("assert");
import { MongoConnectorFactory } from "./MongoConnectorFactory";
describe("connectors/mongo/MongoConnectorFactory", function () {
describe("create", function () {
it("should create a connector", function () {
const factory = new MongoConnectorFactory();
const connector = factory.create("mongodb://test.url");
Assert(connector);
});
});
});

View File

@ -1,12 +0,0 @@
import BluebirdPromise = require("bluebird");
import { IMongoConnectorFactory } from "./IMongoConnectorFactory";
import { IMongoConnector } from "./IMongoConnector";
import { MongoConnector } from "./MongoConnector";
import MongoDB = require("mongodb");
export class MongoConnectorFactory implements IMongoConnectorFactory {
create(url: string): IMongoConnector {
return new MongoConnector(url);
}
}

View File

@ -0,0 +1,38 @@
import Sinon = require("sinon");
import { GlobalLogger } from "./GlobalLogger";
import Winston = require("winston");
import Express = require("express");
import { IGlobalLogger } from "./IGlobalLogger";
export class GlobalLoggerStub implements IGlobalLogger {
infoStub: Sinon.SinonStub;
debugStub: Sinon.SinonStub;
errorStub: Sinon.SinonStub;
private globalLogger: IGlobalLogger;
constructor(enableLogging?: boolean) {
this.infoStub = Sinon.stub();
this.debugStub = Sinon.stub();
this.errorStub = Sinon.stub();
if (enableLogging)
this.globalLogger = new GlobalLogger(Winston);
}
info(message: string, ...args: any[]): void {
if (this.globalLogger)
this.globalLogger.info(message, ...args);
this.infoStub(message, ...args);
}
debug(message: string, ...args: any[]): void {
if (this.globalLogger)
this.globalLogger.info(message, ...args);
this.debugStub(message, ...args);
}
error(message: string, ...args: any[]): void {
if (this.globalLogger)
this.globalLogger.info(message, ...args);
this.errorStub(message, ...args);
}
}

View File

@ -7,14 +7,17 @@ import { MongoCollection } from "./MongoCollection";
describe("storage/mongo/MongoCollection", function () {
let mongoCollectionStub: any;
let mongoClientStub: MongoClientStub;
let findStub: Sinon.SinonStub;
let findOneStub: Sinon.SinonStub;
let insertStub: Sinon.SinonStub;
let updateStub: Sinon.SinonStub;
let removeStub: Sinon.SinonStub;
let countStub: Sinon.SinonStub;
const COLLECTION_NAME = "collection";
before(function () {
mongoClientStub = new MongoClientStub();
mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any);
findStub = mongoCollectionStub.find as Sinon.SinonStub;
findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub;
@ -22,15 +25,18 @@ describe("storage/mongo/MongoCollection", function () {
updateStub = mongoCollectionStub.update as Sinon.SinonStub;
removeStub = mongoCollectionStub.remove as Sinon.SinonStub;
countStub = mongoCollectionStub.count as Sinon.SinonStub;
mongoClientStub.collectionStub.returns(
BluebirdPromise.resolve(mongoCollectionStub)
);
});
describe("find", function () {
it("should find a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub);
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
findStub.returns({
sort: Sinon.stub().returns({
limit: Sinon.stub().returns({
toArray: Sinon.stub().yields(undefined, [])
toArray: Sinon.stub().returns(BluebirdPromise.resolve([]))
})
})
});
@ -44,8 +50,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("findOne", function () {
it("should find one document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub);
findOneStub.yields(undefined, {});
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
findOneStub.returns(BluebirdPromise.resolve({}));
return collection.findOne({ key: "KEY" })
.then(function () {
@ -56,8 +62,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("insert", function () {
it("should insert a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub);
insertStub.yields(undefined, {});
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
insertStub.returns(BluebirdPromise.resolve({}));
return collection.insert({ key: "KEY" })
.then(function () {
@ -68,8 +74,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("update", function () {
it("should update a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub);
updateStub.yields(undefined, {});
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
updateStub.returns(BluebirdPromise.resolve({}));
return collection.update({ key: "KEY" }, { key: "KEY", value: 1 })
.then(function () {
@ -80,8 +86,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("remove", function () {
it("should remove a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub);
removeStub.yields(undefined, {});
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
removeStub.returns(BluebirdPromise.resolve({}));
return collection.remove({ key: "KEY" })
.then(function () {
@ -92,8 +98,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("count", function () {
it("should count documents in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub);
countStub.yields(undefined, {});
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
countStub.returns(BluebirdPromise.resolve({}));
return collection.count({ key: "KEY" })
.then(function () {

View File

@ -1,44 +1,50 @@
import BluebirdPromise = require("bluebird");
import Bluebird = require("bluebird");
import { ICollection } from "../ICollection";
import MongoDB = require("mongodb");
import { IMongoClient } from "../../connectors/mongo/IMongoClient";
export class MongoCollection implements ICollection {
private collection: MongoDB.Collection;
private mongoClient: IMongoClient;
private collectionName: string;
constructor(collection: MongoDB.Collection) {
this.collection = collection;
constructor(collectionName: string, mongoClient: IMongoClient) {
this.collectionName = collectionName;
this.mongoClient = mongoClient;
}
find(query: any, sortKeys?: any, count?: number): BluebirdPromise<any> {
const q = this.collection.find(query).sort(sortKeys).limit(count);
const toArrayAsync = BluebirdPromise.promisify(q.toArray, { context: q });
return toArrayAsync();
private collection(): Bluebird<MongoDB.Collection> {
return this.mongoClient.collection(this.collectionName);
}
findOne(query: any): BluebirdPromise<any> {
const findOneAsync = BluebirdPromise.promisify<any, any>(this.collection.findOne, { context: this.collection });
return findOneAsync(query);
find(query: any, sortKeys?: any, count?: number): Bluebird<any> {
return this.collection()
.then((collection) => collection.find(query).sort(sortKeys).limit(count))
.then((query) => query.toArray());
}
update(query: any, updateQuery: any, options?: any): BluebirdPromise<any> {
const updateAsync = BluebirdPromise.promisify<any, any, any, any>(this.collection.update, { context: this.collection });
return updateAsync(query, updateQuery, options);
findOne(query: any): Bluebird<any> {
return this.collection()
.then((collection) => collection.findOne(query));
}
remove(query: any): BluebirdPromise<any> {
const removeAsync = BluebirdPromise.promisify<any, any>(this.collection.remove, { context: this.collection });
return removeAsync(query);
update(query: any, updateQuery: any, options?: any): Bluebird<any> {
return this.collection()
.then((collection) => collection.update(query, updateQuery, options));
}
insert(document: any): BluebirdPromise<any> {
const insertAsync = BluebirdPromise.promisify<any, any>(this.collection.insert, { context: this.collection });
return insertAsync(document);
remove(query: any): Bluebird<any> {
return this.collection()
.then((collection) => collection.remove(query));
}
count(query: any): BluebirdPromise<number> {
const countAsync = BluebirdPromise.promisify<any, any>(this.collection.count, { context: this.collection });
return countAsync(query);
insert(document: any): Bluebird<any> {
return this.collection()
.then((collection) => collection.insert(document));
}
count(query: any): Bluebird<number> {
return this.collection()
.then((collection) => collection.count(query));
}
}

View File

@ -14,6 +14,6 @@ export class MongoCollectionFactory implements ICollectionFactory {
}
build(collectionName: string): ICollection {
return new MongoCollection(this.mongoClient.collection(collectionName));
return new MongoCollection(collectionName, this.mongoClient);
}
}

View File

@ -0,0 +1,28 @@
require("chromedriver");
import Environment = require('../environment');
const includes = [
"docker-compose.yml",
"example/compose/docker-compose.base.yml",
"example/compose/mongo/docker-compose.yml",
"example/compose/redis/docker-compose.yml",
"example/compose/nginx/backend/docker-compose.yml",
"example/compose/nginx/portal/docker-compose.yml",
"example/compose/smtp/docker-compose.yml",
"example/compose/httpbin/docker-compose.yml",
"example/compose/ldap/docker-compose.yml"
];
before(function() {
this.timeout(20000);
this.environment = new Environment.Environment(includes);
return this.environment.setup(5000);
});
after(function() {
this.timeout(30000);
if(process.env.KEEP_ENV != "true") {
return this.environment.cleanup();
}
});

View File

@ -0,0 +1,19 @@
require("chromedriver");
import SeleniumWebdriver = require("selenium-webdriver");
import WithDriver from '../helpers/with-driver';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
import LoginAs from '../helpers/login-as';
import VisitPage from '../helpers/visit-page';
describe('Connection retry when mongo fails or restarts', function() {
this.timeout(20000);
WithDriver();
it('should be able to login after mongo restarts', function() {
const that = this;
return that.environment.stop_service("mongo")
.then(() => that.environment.restart_service("authelia", 2000))
.then(() => that.environment.restart_service("mongo"))
.then(() => LoginAs(that.driver, "john"));
})
});

View File

@ -6,36 +6,54 @@ function docker_compose(includes: string[]) {
return `docker-compose ${compose_args}`;
}
export function setup(includes: string[], setupTime: number = 2000): Bluebird<void> {
const command = docker_compose(includes) + ' up -d'
console.log('Starting up environment.');
console.log('Running: %s', command);
export class Environment {
private includes: string[];
constructor(includes: string[]) {
this.includes = includes;
}
return new Bluebird<void>(function(resolve, reject) {
private runCommand(command: string, timeout?: number): Bluebird<void> {
return new Bluebird<void>(function(resolve, reject) {
console.log('[ENVIRONMENT] Running: %s', command);
exec(command, function(err, stdout, stderr) {
if(err) {
reject(err);
return;
}
setTimeout(function() {
resolve();
}, setupTime);
if(err) {
reject(err);
return;
}
if(!timeout) resolve();
else setTimeout(resolve, timeout);
});
});
});
}
}
export function cleanup(includes: string[]): Bluebird<void> {
const command = docker_compose(includes) + ' down';
console.log('Shutting down environment.');
console.log('Running: %s', command);
setup(timeout?: number): Bluebird<void> {
const command = docker_compose(this.includes) + ' up -d'
console.log('[ENVIRONMENT] Starting up...');
return this.runCommand(command, timeout);
}
return new Bluebird<void>(function(resolve, reject) {
exec(command, function(err, stdout, stderr) {
if(err) {
reject(err);
return;
}
resolve();
});
});
cleanup(): Bluebird<void> {
const command = docker_compose(this.includes) + ' down'
console.log('[ENVIRONMENT] Cleaning up...');
return this.runCommand(command);
}
stop_service(serviceName: string): Bluebird<void> {
const command = docker_compose(this.includes) + ' stop ' + serviceName;
console.log('[ENVIRONMENT] Stopping service %s...', serviceName);
return this.runCommand(command);
}
start_service(serviceName: string): Bluebird<void> {
const command = docker_compose(this.includes) + ' start ' + serviceName;
console.log('[ENVIRONMENT] Starting service %s...', serviceName);
return this.runCommand(command);
}
restart_service(serviceName: string, timeout?: number): Bluebird<void> {
const command = docker_compose(this.includes) + ' restart ' + serviceName;
console.log('[ENVIRONMENT] Restarting service %s...', serviceName);
return this.runCommand(command, timeout);
}
}

View File

@ -4,13 +4,15 @@ import BluebirdPromise = require("bluebird");
import ChildProcess = require("child_process");
import { UserDataStore } from "../../../server/src/lib/storage/UserDataStore";
import { CollectionFactoryFactory } from "../../../server/src/lib/storage/CollectionFactoryFactory";
import { MongoConnector } from "../../../server/src/lib/connectors/mongo/MongoConnector";
import { IMongoClient } from "../../../server/src/lib/connectors/mongo/IMongoClient";
import { TotpHandler } from "../../../server/src/lib/authentication/totp/TotpHandler";
import Speakeasy = require("speakeasy");
import Request = require("request-promise");
import { TOTPSecret } from "../../../server/types/TOTPSecret";
import Environment = require("../../environment");
import { MongoClient } from "../../../server/src/lib/connectors/mongo/MongoClient";
import { GlobalLogger } from "../../../server/src/lib/logging/GlobalLogger";
import { GlobalLoggerStub } from "../../../server/src/lib/logging/GlobalLoggerStub.spec";
setDefaultTimeout(30 * 1000);
@ -28,12 +30,14 @@ const includes = [
"example/compose/ldap/docker-compose.yml"
]
const environment = new Environment.Environment(includes);
BeforeAll(function() {
return Environment.setup(includes, 10000);
return environment.setup(10000);
});
AfterAll(function() {
return Environment.cleanup(includes)
return environment.cleanup()
});
Before(function () {
@ -99,19 +103,16 @@ declareNeedsConfiguration("totp_issuer", createCustomTotpIssuerConfiguration);
function registerUser(context: any, username: string) {
let secret: TOTPSecret;
const mongoConnector = new MongoConnector("mongodb://localhost:27017");
return mongoConnector.connect("authelia")
.then(function (mongoClient: IMongoClient) {
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
const userDataStore = new UserDataStore(collectionFactory);
const mongoClient = new MongoClient("mongodb://localhost:27017", "authelia", new GlobalLoggerStub());
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
const userDataStore = new UserDataStore(collectionFactory);
const generator = new TotpHandler(Speakeasy);
secret = generator.generate("user", "authelia.com");
return userDataStore.saveTOTPSecret(username, secret);
})
const generator = new TotpHandler(Speakeasy);
secret = generator.generate("user", "authelia.com");
return userDataStore.saveTOTPSecret(username, secret)
.then(function () {
context.totpSecrets["REGISTERED"] = secret.base32;
return mongoConnector.close();
return mongoClient.close();
});
}

View File

@ -2,10 +2,10 @@ import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click';
import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected';
import LoginAs from './login-as';
export default function(driver: any, user: string) {
return VisitPage(driver, "https://login.example.com:8080/")
.then(() => FillLoginPageAndClick(driver, user, "password"))
export default function(driver: any, user: string, email?: boolean) {
return LoginAs(driver, user)
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor"))
.then(() => RegisterTotp(driver));
.then(() => RegisterTotp(driver, email));
}

9
test/helpers/login-as.ts Normal file
View File

@ -0,0 +1,9 @@
import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click';
import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected';
export default function(driver: any, user: string) {
return VisitPage(driver, "https://login.example.com:8080/")
.then(() => FillLoginPageAndClick(driver, user, "password"));
}

View File

@ -1,6 +1,7 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Request = require("request-promise");
function retrieveValidationLinkFromNotificationFile(): Bluebird<string> {
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
@ -12,13 +13,35 @@ function retrieveValidationLinkFromNotificationFile(): Bluebird<string> {
});
};
export default function(driver: any): Bluebird<string> {
function retrieveValidationLinkFromEmail(): Bluebird<string> {
return Request({
method: "GET",
uri: "http://localhost:8085/messages",
json: true
})
.then(function (data: any) {
const messageId = data[data.length - 1].id;
return Request({
method: "GET",
uri: `http://localhost:8085/messages/${messageId}.html`
});
})
.then(function (data: any) {
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
const match = regexp.exec(data);
const link = match[1];
return Bluebird.resolve(link);
});
};
export default function(driver: any, email?: boolean): Bluebird<string> {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
})
.then(function () {
return retrieveValidationLinkFromNotificationFile();
if(email) return retrieveValidationLinkFromEmail();
else return retrieveValidationLinkFromNotificationFile();
})
.then(function (link: string) {
return driver.get(link);

View File

@ -11,10 +11,11 @@ const includes = [
before(function() {
this.timeout(20000);
return Environment.setup(includes);
this.environment = new Environment.Environment(includes);
return this.environment.setup(2000);
});
after(function() {
this.timeout(30000);
return Environment.cleanup(includes);
return this.environment.cleanup();
});