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", cmd: "./scripts/run-cucumber.sh",
args: ["./test/features"] args: ["./test/features"]
}, },
"test-complete-config": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'test/complete-config/**/*.ts']
},
"test-minimal-config": { "test-minimal-config": {
cmd: "./node_modules/.bin/mocha", cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'test/minimal-config/**/*.ts'] 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-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-client', ['env:env-test-client-unit', 'run:test-client-unit'])
grunt.registerTask('test-unit', ['test-server', 'test-client']); 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('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);
grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']); 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> { private setup(config: Configuration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise<void> {
const that = this; const that = this;
return ServerVariablesInitializer.initialize(config, this.requestLogger, deps) return ServerVariablesInitializer.initialize(
config, this.globalLogger, this.requestLogger, deps)
.then(function (vars: ServerVariables) { .then(function (vars: ServerVariables) {
Configurator.configure(config, app, vars, deps); Configurator.configure(config, app, vars, deps);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();

View File

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

View File

@ -1,5 +1,6 @@
import MongoDB = require("mongodb"); import MongoDB = require("mongodb");
import Bluebird = require("bluebird");
export interface IMongoClient { 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 Assert = require("assert");
import Sinon = require("sinon"); import Sinon = require("sinon");
import MongoDB = require("mongodb"); import MongoDB = require("mongodb");
import Bluebird = require("bluebird");
import { MongoClient } from "./MongoClient"; import { MongoClient } from "./MongoClient";
import { GlobalLoggerStub } from "../../logging/GlobalLoggerStub.spec";
describe("connectors/mongo/MongoClient", function () { describe("connectors/mongo/MongoClient", function () {
let mongoClientConnectStub: Sinon.SinonStub; let MongoClientStub: any;
let mongoDatabase: any; let mongoClientStub: any;
let mongoDatabaseCollectionStub: Sinon.SinonStub; let mongoDatabaseStub: any;
let logger: GlobalLoggerStub = new GlobalLoggerStub();
describe("collection", function () { describe("collection", function () {
before(function () { before(function() {
mongoDatabaseCollectionStub = Sinon.stub(); mongoClientStub = {
mongoDatabase = { db: Sinon.stub()
collection: mongoDatabaseCollectionStub
}; };
mongoDatabaseStub = {
mongoClientConnectStub = Sinon.stub(MongoDB.MongoClient, "connect"); on: Sinon.stub(),
mongoClientConnectStub.yields(undefined, mongoDatabase); collection: Sinon.stub()
}
}); });
after(function () { describe("Connection to mongo is ok", function() {
mongoClientConnectStub.restore(); 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 () { describe("Connection to mongo is broken", function() {
const COLLECTION_NAME = "mycollection"; before(function () {
const client = new MongoClient(mongoDatabase); MongoClientStub = Sinon.stub(
MongoDB.MongoClient, "connect");
MongoClientStub.yields(
new Error("Failed connection"), undefined);
});
after(function () {
MongoClientStub.restore();
});
mongoDatabaseCollectionStub.returns({}); it("should fail creating the collection", function() {
const COLLECTION_NAME = "mycollection";
const collection = client.collection(COLLECTION_NAME); const client = new MongoClient("mongo://url", "databasename", logger);
Assert(collection); mongoDatabaseStub.collection.returns("COL");
Assert(mongoDatabaseCollectionStub.calledWith(COLLECTION_NAME )); 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 MongoDB = require("mongodb");
import { IMongoClient } from "./IMongoClient"; import { IMongoClient } from "./IMongoClient";
import Bluebird = require("bluebird");
import { AUTHENTICATION_FAILED } from "../../../../../shared/UserMessages";
import { IGlobalLogger } from "../../logging/IGlobalLogger";
export class MongoClient implements IMongoClient { export class MongoClient implements IMongoClient {
private db: MongoDB.Db; private url: string;
private databaseName: string;
constructor(db: MongoDB.Db) { private database: MongoDB.Db;
this.db = 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 { connect(): Bluebird<void> {
return this.db.collection(name); 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 Sinon = require("sinon");
import MongoDB = require("mongodb"); import MongoDB = require("mongodb");
import Bluebird = require("bluebird");
import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient"; import { IMongoClient } from "../../../../src/lib/connectors/mongo/IMongoClient";
export class MongoClientStub implements IMongoClient { export class MongoClientStub implements IMongoClient {
@ -9,7 +10,7 @@ export class MongoClientStub implements IMongoClient {
this.collectionStub = Sinon.stub(); this.collectionStub = Sinon.stub();
} }
collection(name: string): MongoDB.Collection { collection(name: string): Bluebird<MongoDB.Collection> {
return this.collectionStub(name); 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 () { describe("storage/mongo/MongoCollection", function () {
let mongoCollectionStub: any; let mongoCollectionStub: any;
let mongoClientStub: MongoClientStub;
let findStub: Sinon.SinonStub; let findStub: Sinon.SinonStub;
let findOneStub: Sinon.SinonStub; let findOneStub: Sinon.SinonStub;
let insertStub: Sinon.SinonStub; let insertStub: Sinon.SinonStub;
let updateStub: Sinon.SinonStub; let updateStub: Sinon.SinonStub;
let removeStub: Sinon.SinonStub; let removeStub: Sinon.SinonStub;
let countStub: Sinon.SinonStub; let countStub: Sinon.SinonStub;
const COLLECTION_NAME = "collection";
before(function () { before(function () {
mongoClientStub = new MongoClientStub();
mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any);
findStub = mongoCollectionStub.find as Sinon.SinonStub; findStub = mongoCollectionStub.find as Sinon.SinonStub;
findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub;
@ -22,15 +25,18 @@ describe("storage/mongo/MongoCollection", function () {
updateStub = mongoCollectionStub.update as Sinon.SinonStub; updateStub = mongoCollectionStub.update as Sinon.SinonStub;
removeStub = mongoCollectionStub.remove as Sinon.SinonStub; removeStub = mongoCollectionStub.remove as Sinon.SinonStub;
countStub = mongoCollectionStub.count as Sinon.SinonStub; countStub = mongoCollectionStub.count as Sinon.SinonStub;
mongoClientStub.collectionStub.returns(
BluebirdPromise.resolve(mongoCollectionStub)
);
}); });
describe("find", function () { describe("find", function () {
it("should find a document in the collection", function () { it("should find a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
findStub.returns({ findStub.returns({
sort: Sinon.stub().returns({ sort: Sinon.stub().returns({
limit: 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 () { describe("findOne", function () {
it("should find one document in the collection", function () { it("should find one document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
findOneStub.yields(undefined, {}); findOneStub.returns(BluebirdPromise.resolve({}));
return collection.findOne({ key: "KEY" }) return collection.findOne({ key: "KEY" })
.then(function () { .then(function () {
@ -56,8 +62,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("insert", function () { describe("insert", function () {
it("should insert a document in the collection", function () { it("should insert a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
insertStub.yields(undefined, {}); insertStub.returns(BluebirdPromise.resolve({}));
return collection.insert({ key: "KEY" }) return collection.insert({ key: "KEY" })
.then(function () { .then(function () {
@ -68,8 +74,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("update", function () { describe("update", function () {
it("should update a document in the collection", function () { it("should update a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
updateStub.yields(undefined, {}); updateStub.returns(BluebirdPromise.resolve({}));
return collection.update({ key: "KEY" }, { key: "KEY", value: 1 }) return collection.update({ key: "KEY" }, { key: "KEY", value: 1 })
.then(function () { .then(function () {
@ -80,8 +86,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("remove", function () { describe("remove", function () {
it("should remove a document in the collection", function () { it("should remove a document in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
removeStub.yields(undefined, {}); removeStub.returns(BluebirdPromise.resolve({}));
return collection.remove({ key: "KEY" }) return collection.remove({ key: "KEY" })
.then(function () { .then(function () {
@ -92,8 +98,8 @@ describe("storage/mongo/MongoCollection", function () {
describe("count", function () { describe("count", function () {
it("should count documents in the collection", function () { it("should count documents in the collection", function () {
const collection = new MongoCollection(mongoCollectionStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
countStub.yields(undefined, {}); countStub.returns(BluebirdPromise.resolve({}));
return collection.count({ key: "KEY" }) return collection.count({ key: "KEY" })
.then(function () { .then(function () {

View File

@ -1,44 +1,50 @@
import Bluebird = require("bluebird");
import BluebirdPromise = require("bluebird");
import { ICollection } from "../ICollection"; import { ICollection } from "../ICollection";
import MongoDB = require("mongodb"); import MongoDB = require("mongodb");
import { IMongoClient } from "../../connectors/mongo/IMongoClient";
export class MongoCollection implements ICollection { export class MongoCollection implements ICollection {
private collection: MongoDB.Collection; private mongoClient: IMongoClient;
private collectionName: string;
constructor(collection: MongoDB.Collection) { constructor(collectionName: string, mongoClient: IMongoClient) {
this.collection = collection; this.collectionName = collectionName;
this.mongoClient = mongoClient;
} }
find(query: any, sortKeys?: any, count?: number): BluebirdPromise<any> { private collection(): Bluebird<MongoDB.Collection> {
const q = this.collection.find(query).sort(sortKeys).limit(count); return this.mongoClient.collection(this.collectionName);
const toArrayAsync = BluebirdPromise.promisify(q.toArray, { context: q });
return toArrayAsync();
} }
findOne(query: any): BluebirdPromise<any> { find(query: any, sortKeys?: any, count?: number): Bluebird<any> {
const findOneAsync = BluebirdPromise.promisify<any, any>(this.collection.findOne, { context: this.collection }); return this.collection()
return findOneAsync(query); .then((collection) => collection.find(query).sort(sortKeys).limit(count))
.then((query) => query.toArray());
} }
update(query: any, updateQuery: any, options?: any): BluebirdPromise<any> { findOne(query: any): Bluebird<any> {
const updateAsync = BluebirdPromise.promisify<any, any, any, any>(this.collection.update, { context: this.collection }); return this.collection()
return updateAsync(query, updateQuery, options); .then((collection) => collection.findOne(query));
} }
remove(query: any): BluebirdPromise<any> { update(query: any, updateQuery: any, options?: any): Bluebird<any> {
const removeAsync = BluebirdPromise.promisify<any, any>(this.collection.remove, { context: this.collection }); return this.collection()
return removeAsync(query); .then((collection) => collection.update(query, updateQuery, options));
} }
insert(document: any): BluebirdPromise<any> { remove(query: any): Bluebird<any> {
const insertAsync = BluebirdPromise.promisify<any, any>(this.collection.insert, { context: this.collection }); return this.collection()
return insertAsync(document); .then((collection) => collection.remove(query));
} }
count(query: any): BluebirdPromise<number> { insert(document: any): Bluebird<any> {
const countAsync = BluebirdPromise.promisify<any, any>(this.collection.count, { context: this.collection }); return this.collection()
return countAsync(query); .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 { 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}`; return `docker-compose ${compose_args}`;
} }
export function setup(includes: string[], setupTime: number = 2000): Bluebird<void> { export class Environment {
const command = docker_compose(includes) + ' up -d' private includes: string[];
console.log('Starting up environment.'); constructor(includes: string[]) {
console.log('Running: %s', command); 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) { exec(command, function(err, stdout, stderr) {
if(err) { if(err) {
reject(err); reject(err);
return; return;
} }
setTimeout(function() { if(!timeout) resolve();
resolve(); else setTimeout(resolve, timeout);
}, setupTime); });
}); });
}); }
}
export function cleanup(includes: string[]): Bluebird<void> { setup(timeout?: number): Bluebird<void> {
const command = docker_compose(includes) + ' down'; const command = docker_compose(this.includes) + ' up -d'
console.log('Shutting down environment.'); console.log('[ENVIRONMENT] Starting up...');
console.log('Running: %s', command); return this.runCommand(command, timeout);
}
return new Bluebird<void>(function(resolve, reject) { cleanup(): Bluebird<void> {
exec(command, function(err, stdout, stderr) { const command = docker_compose(this.includes) + ' down'
if(err) { console.log('[ENVIRONMENT] Cleaning up...');
reject(err); return this.runCommand(command);
return; }
}
resolve(); 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 ChildProcess = require("child_process");
import { UserDataStore } from "../../../server/src/lib/storage/UserDataStore"; import { UserDataStore } from "../../../server/src/lib/storage/UserDataStore";
import { CollectionFactoryFactory } from "../../../server/src/lib/storage/CollectionFactoryFactory"; 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 { IMongoClient } from "../../../server/src/lib/connectors/mongo/IMongoClient";
import { TotpHandler } from "../../../server/src/lib/authentication/totp/TotpHandler"; import { TotpHandler } from "../../../server/src/lib/authentication/totp/TotpHandler";
import Speakeasy = require("speakeasy"); import Speakeasy = require("speakeasy");
import Request = require("request-promise"); import Request = require("request-promise");
import { TOTPSecret } from "../../../server/types/TOTPSecret"; import { TOTPSecret } from "../../../server/types/TOTPSecret";
import Environment = require("../../environment"); 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); setDefaultTimeout(30 * 1000);
@ -28,12 +30,14 @@ const includes = [
"example/compose/ldap/docker-compose.yml" "example/compose/ldap/docker-compose.yml"
] ]
const environment = new Environment.Environment(includes);
BeforeAll(function() { BeforeAll(function() {
return Environment.setup(includes, 10000); return environment.setup(10000);
}); });
AfterAll(function() { AfterAll(function() {
return Environment.cleanup(includes) return environment.cleanup()
}); });
Before(function () { Before(function () {
@ -99,19 +103,16 @@ declareNeedsConfiguration("totp_issuer", createCustomTotpIssuerConfiguration);
function registerUser(context: any, username: string) { function registerUser(context: any, username: string) {
let secret: TOTPSecret; let secret: TOTPSecret;
const mongoConnector = new MongoConnector("mongodb://localhost:27017"); const mongoClient = new MongoClient("mongodb://localhost:27017", "authelia", new GlobalLoggerStub());
return mongoConnector.connect("authelia") const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
.then(function (mongoClient: IMongoClient) { const userDataStore = new UserDataStore(collectionFactory);
const collectionFactory = CollectionFactoryFactory.createMongo(mongoClient);
const userDataStore = new UserDataStore(collectionFactory);
const generator = new TotpHandler(Speakeasy); const generator = new TotpHandler(Speakeasy);
secret = generator.generate("user", "authelia.com"); secret = generator.generate("user", "authelia.com");
return userDataStore.saveTOTPSecret(username, secret); return userDataStore.saveTOTPSecret(username, secret)
})
.then(function () { .then(function () {
context.totpSecrets["REGISTERED"] = secret.base32; 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 FillLoginPageAndClick from './fill-login-page-and-click';
import RegisterTotp from './register-totp'; import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected'; import WaitRedirected from './wait-redirected';
import LoginAs from './login-as';
export default function(driver: any, user: string) { export default function(driver: any, user: string, email?: boolean) {
return VisitPage(driver, "https://login.example.com:8080/") return LoginAs(driver, user)
.then(() => FillLoginPageAndClick(driver, user, "password"))
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor")) .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 Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver"); import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs"); import Fs = require("fs");
import Request = require("request-promise");
function retrieveValidationLinkFromNotificationFile(): Bluebird<string> { function retrieveValidationLinkFromNotificationFile(): Bluebird<string> {
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt") 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) return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
.then(function () { .then(function () {
return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click(); return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
}) })
.then(function () { .then(function () {
return retrieveValidationLinkFromNotificationFile(); if(email) return retrieveValidationLinkFromEmail();
else return retrieveValidationLinkFromNotificationFile();
}) })
.then(function (link: string) { .then(function (link: string) {
return driver.get(link); return driver.get(link);

View File

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