From c503765dd62ee2f44763e2097fbd12cf4a22f78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Michaud?= Date: Sun, 19 Aug 2018 16:51:36 +0200 Subject: [PATCH] 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. --- Gruntfile.js | 6 +- server/src/lib/Server.ts | 3 +- server/src/lib/ServerVariablesInitializer.ts | 26 ++++--- .../lib/connectors/mongo/IMongoClient.d.ts | 3 +- .../lib/connectors/mongo/IMongoConnector.d.ts | 7 -- .../mongo/IMongoConnectorFactory.d.ts | 5 -- .../lib/connectors/mongo/MongoClient.spec.ts | 78 +++++++++++++------ .../src/lib/connectors/mongo/MongoClient.ts | 55 +++++++++++-- .../connectors/mongo/MongoClientStub.spec.ts | 3 +- .../connectors/mongo/MongoConnector.spec.ts | 47 ----------- .../lib/connectors/mongo/MongoConnector.ts | 31 -------- .../mongo/MongoConnectorFactory.spec.ts | 13 ---- .../connectors/mongo/MongoConnectorFactory.ts | 12 --- .../src/lib/logging/GlobalLoggerStub.spec.ts | 38 +++++++++ .../lib/storage/mongo/MongoCollection.spec.ts | 30 ++++--- .../src/lib/storage/mongo/MongoCollection.ts | 54 +++++++------ .../storage/mongo/MongoCollectionFactory.ts | 2 +- test/complete-config/00-suite.ts | 28 +++++++ .../mongo-broken-connection.ts | 19 +++++ test/environment.ts | 72 ++++++++++------- test/features/step_definitions/hooks.ts | 27 +++---- test/helpers/login-and-register-totp.ts | 8 +- test/helpers/login-as.ts | 9 +++ test/helpers/register-totp.ts | 27 ++++++- test/minimal-config/00-suite.ts | 5 +- 25 files changed, 366 insertions(+), 242 deletions(-) delete mode 100644 server/src/lib/connectors/mongo/IMongoConnector.d.ts delete mode 100644 server/src/lib/connectors/mongo/IMongoConnectorFactory.d.ts delete mode 100644 server/src/lib/connectors/mongo/MongoConnector.spec.ts delete mode 100644 server/src/lib/connectors/mongo/MongoConnector.ts delete mode 100644 server/src/lib/connectors/mongo/MongoConnectorFactory.spec.ts delete mode 100644 server/src/lib/connectors/mongo/MongoConnectorFactory.ts create mode 100644 server/src/lib/logging/GlobalLoggerStub.spec.ts create mode 100644 test/complete-config/00-suite.ts create mode 100644 test/complete-config/mongo-broken-connection.ts create mode 100644 test/helpers/login-as.ts diff --git a/Gruntfile.js b/Gruntfile.js index 130d01700..97e2d3da6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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']); diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index 6bd1e2989..cffb2d9bb 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -48,7 +48,8 @@ export default class Server { private setup(config: Configuration, app: Express.Application, deps: GlobalDependencies): BluebirdPromise { 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(); diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts index 2a9f40977..0f7209966 100644 --- a/server/src/lib/ServerVariablesInitializer.ts +++ b/server/src/lib/ServerVariablesInitializer.ts @@ -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 { + static create(config: Configuration.Configuration, globalLogger: IGlobalLogger): BluebirdPromise { 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 { + 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); diff --git a/server/src/lib/connectors/mongo/IMongoClient.d.ts b/server/src/lib/connectors/mongo/IMongoClient.d.ts index b964ae752..36cb4b8bf 100644 --- a/server/src/lib/connectors/mongo/IMongoClient.d.ts +++ b/server/src/lib/connectors/mongo/IMongoClient.d.ts @@ -1,5 +1,6 @@ import MongoDB = require("mongodb"); +import Bluebird = require("bluebird"); export interface IMongoClient { - collection(name: string): MongoDB.Collection; + collection(name: string): Bluebird } \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/IMongoConnector.d.ts b/server/src/lib/connectors/mongo/IMongoConnector.d.ts deleted file mode 100644 index d3403f0a9..000000000 --- a/server/src/lib/connectors/mongo/IMongoConnector.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import BluebirdPromise = require("bluebird"); -import { IMongoClient } from "./IMongoClient"; - -export interface IMongoConnector { - connect(databaseName: string): BluebirdPromise; - close(): BluebirdPromise; -} \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/IMongoConnectorFactory.d.ts b/server/src/lib/connectors/mongo/IMongoConnectorFactory.d.ts deleted file mode 100644 index b69b9271c..000000000 --- a/server/src/lib/connectors/mongo/IMongoConnectorFactory.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IMongoConnector } from "./IMongoConnector"; - -export interface IMongoConnectorFactory { - create(url: string): IMongoConnector; -} \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/MongoClient.spec.ts b/server/src/lib/connectors/mongo/MongoClient.spec.ts index 355e6f028..da3e8d622 100644 --- a/server/src/lib/connectors/mongo/MongoClient.spec.ts +++ b/server/src/lib/connectors/mongo/MongoClient.spec.ts @@ -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()); + }); + }) }); }); diff --git a/server/src/lib/connectors/mongo/MongoClient.ts b/server/src/lib/connectors/mongo/MongoClient.ts index 6d227868e..d4570e7ae 100644 --- a/server/src/lib/connectors/mongo/MongoClient.ts +++ b/server/src/lib/connectors/mongo/MongoClient.ts @@ -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 { + 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 { + if (this.client) { + this.client.close(); + this.database = undefined; + this.client = undefined; + } + return Bluebird.resolve(); + } + + collection(name: string): Bluebird { + if (!this.client) { + const that = this; + return this.connect() + .then(() => Bluebird.resolve(that.database.collection(name))); + } + + return Bluebird.resolve(this.database.collection(name)); } } \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/MongoClientStub.spec.ts b/server/src/lib/connectors/mongo/MongoClientStub.spec.ts index 81999b00e..1cfd48e32 100644 --- a/server/src/lib/connectors/mongo/MongoClientStub.spec.ts +++ b/server/src/lib/connectors/mongo/MongoClientStub.spec.ts @@ -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 { return this.collectionStub(name); } } \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/MongoConnector.spec.ts b/server/src/lib/connectors/mongo/MongoConnector.spec.ts deleted file mode 100644 index a18026977..000000000 --- a/server/src/lib/connectors/mongo/MongoConnector.spec.ts +++ /dev/null @@ -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(); - }); - }); - }); -}); diff --git a/server/src/lib/connectors/mongo/MongoConnector.ts b/server/src/lib/connectors/mongo/MongoConnector.ts deleted file mode 100644 index e6a82d4b6..000000000 --- a/server/src/lib/connectors/mongo/MongoConnector.ts +++ /dev/null @@ -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 { - 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 { - this.client.close(); - return BluebirdPromise.resolve(); - } -} \ No newline at end of file diff --git a/server/src/lib/connectors/mongo/MongoConnectorFactory.spec.ts b/server/src/lib/connectors/mongo/MongoConnectorFactory.spec.ts deleted file mode 100644 index d82570a8c..000000000 --- a/server/src/lib/connectors/mongo/MongoConnectorFactory.spec.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/server/src/lib/connectors/mongo/MongoConnectorFactory.ts b/server/src/lib/connectors/mongo/MongoConnectorFactory.ts deleted file mode 100644 index 9f57e1080..000000000 --- a/server/src/lib/connectors/mongo/MongoConnectorFactory.ts +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/server/src/lib/logging/GlobalLoggerStub.spec.ts b/server/src/lib/logging/GlobalLoggerStub.spec.ts new file mode 100644 index 000000000..d4bb13710 --- /dev/null +++ b/server/src/lib/logging/GlobalLoggerStub.spec.ts @@ -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); + } +} \ No newline at end of file diff --git a/server/src/lib/storage/mongo/MongoCollection.spec.ts b/server/src/lib/storage/mongo/MongoCollection.spec.ts index 49208b5df..9838c21c7 100644 --- a/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ b/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -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 () { diff --git a/server/src/lib/storage/mongo/MongoCollection.ts b/server/src/lib/storage/mongo/MongoCollection.ts index 8b96751b1..3d6a65e9d 100644 --- a/server/src/lib/storage/mongo/MongoCollection.ts +++ b/server/src/lib/storage/mongo/MongoCollection.ts @@ -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 { - const q = this.collection.find(query).sort(sortKeys).limit(count); - const toArrayAsync = BluebirdPromise.promisify(q.toArray, { context: q }); - return toArrayAsync(); + private collection(): Bluebird { + return this.mongoClient.collection(this.collectionName); } - findOne(query: any): BluebirdPromise { - const findOneAsync = BluebirdPromise.promisify(this.collection.findOne, { context: this.collection }); - return findOneAsync(query); + find(query: any, sortKeys?: any, count?: number): Bluebird { + return this.collection() + .then((collection) => collection.find(query).sort(sortKeys).limit(count)) + .then((query) => query.toArray()); } - update(query: any, updateQuery: any, options?: any): BluebirdPromise { - const updateAsync = BluebirdPromise.promisify(this.collection.update, { context: this.collection }); - return updateAsync(query, updateQuery, options); + findOne(query: any): Bluebird { + return this.collection() + .then((collection) => collection.findOne(query)); } - remove(query: any): BluebirdPromise { - const removeAsync = BluebirdPromise.promisify(this.collection.remove, { context: this.collection }); - return removeAsync(query); + update(query: any, updateQuery: any, options?: any): Bluebird { + return this.collection() + .then((collection) => collection.update(query, updateQuery, options)); } - insert(document: any): BluebirdPromise { - const insertAsync = BluebirdPromise.promisify(this.collection.insert, { context: this.collection }); - return insertAsync(document); + remove(query: any): Bluebird { + return this.collection() + .then((collection) => collection.remove(query)); } - count(query: any): BluebirdPromise { - const countAsync = BluebirdPromise.promisify(this.collection.count, { context: this.collection }); - return countAsync(query); + insert(document: any): Bluebird { + return this.collection() + .then((collection) => collection.insert(document)); + } + + count(query: any): Bluebird { + return this.collection() + .then((collection) => collection.count(query)); } } \ No newline at end of file diff --git a/server/src/lib/storage/mongo/MongoCollectionFactory.ts b/server/src/lib/storage/mongo/MongoCollectionFactory.ts index 6738ef1aa..14a8262c9 100644 --- a/server/src/lib/storage/mongo/MongoCollectionFactory.ts +++ b/server/src/lib/storage/mongo/MongoCollectionFactory.ts @@ -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); } } \ No newline at end of file diff --git a/test/complete-config/00-suite.ts b/test/complete-config/00-suite.ts new file mode 100644 index 000000000..3ef451b56 --- /dev/null +++ b/test/complete-config/00-suite.ts @@ -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(); + } +}); \ No newline at end of file diff --git a/test/complete-config/mongo-broken-connection.ts b/test/complete-config/mongo-broken-connection.ts new file mode 100644 index 000000000..e14c504bf --- /dev/null +++ b/test/complete-config/mongo-broken-connection.ts @@ -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")); + }) +}); diff --git a/test/environment.ts b/test/environment.ts index e52f405db..05f347cf5 100644 --- a/test/environment.ts +++ b/test/environment.ts @@ -6,36 +6,54 @@ function docker_compose(includes: string[]) { return `docker-compose ${compose_args}`; } -export function setup(includes: string[], setupTime: number = 2000): Bluebird { - 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(function(resolve, reject) { + private runCommand(command: string, timeout?: number): Bluebird { + return new Bluebird(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 { - const command = docker_compose(includes) + ' down'; - console.log('Shutting down environment.'); - console.log('Running: %s', command); + setup(timeout?: number): Bluebird { + const command = docker_compose(this.includes) + ' up -d' + console.log('[ENVIRONMENT] Starting up...'); + return this.runCommand(command, timeout); + } - return new Bluebird(function(resolve, reject) { - exec(command, function(err, stdout, stderr) { - if(err) { - reject(err); - return; - } - resolve(); - }); - }); + cleanup(): Bluebird { + const command = docker_compose(this.includes) + ' down' + console.log('[ENVIRONMENT] Cleaning up...'); + return this.runCommand(command); + } + + stop_service(serviceName: string): Bluebird { + const command = docker_compose(this.includes) + ' stop ' + serviceName; + console.log('[ENVIRONMENT] Stopping service %s...', serviceName); + return this.runCommand(command); + } + + start_service(serviceName: string): Bluebird { + 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 { + const command = docker_compose(this.includes) + ' restart ' + serviceName; + console.log('[ENVIRONMENT] Restarting service %s...', serviceName); + return this.runCommand(command, timeout); + } } \ No newline at end of file diff --git a/test/features/step_definitions/hooks.ts b/test/features/step_definitions/hooks.ts index eb46b8a8f..095a996b6 100644 --- a/test/features/step_definitions/hooks.ts +++ b/test/features/step_definitions/hooks.ts @@ -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(); }); } diff --git a/test/helpers/login-and-register-totp.ts b/test/helpers/login-and-register-totp.ts index 18a24716e..f4f99bf55 100644 --- a/test/helpers/login-and-register-totp.ts +++ b/test/helpers/login-and-register-totp.ts @@ -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)); } \ No newline at end of file diff --git a/test/helpers/login-as.ts b/test/helpers/login-as.ts new file mode 100644 index 000000000..6a22699b4 --- /dev/null +++ b/test/helpers/login-as.ts @@ -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")); +} \ No newline at end of file diff --git a/test/helpers/register-totp.ts b/test/helpers/register-totp.ts index 9265a1dfd..9828c8cec 100644 --- a/test/helpers/register-totp.ts +++ b/test/helpers/register-totp.ts @@ -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 { return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt") @@ -12,13 +13,35 @@ function retrieveValidationLinkFromNotificationFile(): Bluebird { }); }; -export default function(driver: any): Bluebird { +function retrieveValidationLinkFromEmail(): Bluebird { + 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(/Continue<\/a>/); + const match = regexp.exec(data); + const link = match[1]; + return Bluebird.resolve(link); + }); +}; + +export default function(driver: any, email?: boolean): Bluebird { 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); diff --git a/test/minimal-config/00-suite.ts b/test/minimal-config/00-suite.ts index 7812bd458..71f8db899 100644 --- a/test/minimal-config/00-suite.ts +++ b/test/minimal-config/00-suite.ts @@ -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(); }); \ No newline at end of file