From e62f10b876188ec16339c121ec549a1799daa0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 26 Jul 2018 16:27:06 +0200 Subject: [PATCH] Update OpenIddictMongoDbContext to use SemaphoreSlim to avoid multiple executions of the initialization routine --- .../OpenIddictMongoDbBuilder.cs | 9 + .../OpenIddictMongoDbContext.cs | 97 ++++--- .../OpenIddictMongoDbOptions.cs | 7 + ...OpenIddictApplicationStoreResolverTests.cs | 16 +- ...enIddictAuthorizationStoreResolverTests.cs | 16 +- .../OpenIddictScopeStoreResolverTests.cs | 16 +- .../OpenIddictTokenStoreResolverTests.cs | 16 +- ...OpenIddictApplicationStoreResolverTests.cs | 16 +- ...enIddictAuthorizationStoreResolverTests.cs | 16 +- .../OpenIddictScopeStoreResolverTests.cs | 16 +- .../OpenIddictTokenStoreResolverTests.cs | 16 +- .../OpenIddictMongoDbBuilderTests.cs | 17 ++ .../OpenIddictMongoDbContextTests.cs | 261 ++++++++++++++++++ 13 files changed, 416 insertions(+), 103 deletions(-) create mode 100644 test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs index 22899343..314c3d01 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs @@ -135,6 +135,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.AuthorizationsCollectionName = name); } + /// + /// Sets the maximal duration given to the MongoDB client to initialize + /// the database and register the indexes used by the OpenIddict entities. + /// + /// The timeout. + /// The . + public OpenIddictMongoDbBuilder SetInitializationTimeout(TimeSpan timeout) + => Configure(options => options.InitializationTimeout = timeout); + /// /// Replaces the default scopes collection name (by default, openiddict.scopes). /// diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs index 9c8bd953..4c09652b 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs @@ -23,6 +23,7 @@ namespace OpenIddict.MongoDb { private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; + private readonly SemaphoreSlim _semaphore; private IMongoDatabase _database; public OpenIddictMongoDbContext( @@ -31,6 +32,7 @@ namespace OpenIddict.MongoDb { _options = options; _provider = provider; + _semaphore = new SemaphoreSlim(1); } /// @@ -55,56 +57,73 @@ namespace OpenIddict.MongoDb throw new InvalidOperationException("The OpenIddict MongoDB options cannot be retrieved."); } - var database = options.Database; - if (database == null) - { - database = _provider.GetService(); - } - - if (database == null) + if (!await _semaphore.WaitAsync(options.InitializationTimeout, cancellationToken)) { throw new InvalidOperationException(new StringBuilder() - .AppendLine("No suitable MongoDB database service can be found.") - .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ") - .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ") - .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.") + .AppendLine("The MongoDB database couldn't be initialized within a reasonable timeframe.") + .Append("Make sure that the MongoDB server is ready and accepts connections from this machine ") + .Append("or use 'options.UseMongoDb().SetInitializationTimeout()' to manually adjust the timeout.") .ToString()); } - // Note: the cancellation token passed as a parameter is deliberately not used here to ensure - // the cancellation of a single store operation doesn't prevent the indexes from being created. - var applications = database.GetCollection(options.ApplicationsCollectionName); - await applications.Indexes.CreateOneAsync( - Builders.IndexKeys.Ascending(application => application.ClientId), - new CreateIndexOptions + try + { + var database = options.Database; + if (database == null) { - Unique = true - }); + database = _provider.GetService(); + } - await applications.Indexes.CreateOneAsync( - Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris)); + if (database == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No suitable MongoDB database service can be found.") + .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ") + .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ") + .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.") + .ToString()); + } - await applications.Indexes.CreateOneAsync( - Builders.IndexKeys.Ascending(application => application.RedirectUris)); + // Note: the cancellation token passed as a parameter is deliberately not used here to ensure + // the cancellation of a single store operation doesn't prevent the indexes from being created. + var applications = database.GetCollection(options.ApplicationsCollectionName); + await applications.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(application => application.ClientId), + new CreateIndexOptions + { + Unique = true + }); - var scopes = database.GetCollection(options.ScopesCollectionName); - await scopes.Indexes.CreateOneAsync( - Builders.IndexKeys.Ascending(scope => scope.Name), - new CreateIndexOptions - { - Unique = true - }); + await applications.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris)); - var tokens = database.GetCollection(options.TokensCollectionName); - await tokens.Indexes.CreateOneAsync( - Builders.IndexKeys.Ascending(token => token.ReferenceId), - new CreateIndexOptions - { - PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId), - Unique = true - }); + await applications.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(application => application.RedirectUris)); - return _database = database; + var scopes = database.GetCollection(options.ScopesCollectionName); + await scopes.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(scope => scope.Name), + new CreateIndexOptions + { + Unique = true + }); + + var tokens = database.GetCollection(options.TokensCollectionName); + await tokens.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(token => token.ReferenceId), + new CreateIndexOptions + { + PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId), + Unique = true + }); + + return _database = database; + } + + finally + { + _semaphore.Release(); + } } return new ValueTask(ExecuteAsync()); diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs index d8abbec3..326eaa09 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System; using MongoDB.Driver; namespace OpenIddict.MongoDb @@ -29,6 +30,12 @@ namespace OpenIddict.MongoDb /// public IMongoDatabase Database { get; set; } + /// + /// Gets or sets the maximal duration given to the MongoDB client to initialize + /// the database and register the indexes used by the OpenIddict entities. + /// + public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// /// Gets or sets the name of the scopes collection (by default, openiddict.scopes). /// diff --git a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs index 4ee59a3b..f21ad750 100644 --- a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFramework.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFramework.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs index d6a37585..2ac870fd 100644 --- a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFramework.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFramework.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs index 2e158a0c..1e66222a 100644 --- a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFramework.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFramework.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs index c4c7d884..cfe13b5d 100644 --- a/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFramework.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFramework.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFramework.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs index 87c84b7d..9e6bfe4c 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictApplicationStoreResolver(monitor, provider); + var resolver = new OpenIddictApplicationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs index 376c91f8..ef04c8d7 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictAuthorizationStoreResolver(monitor, provider); + var resolver = new OpenIddictAuthorizationStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs index dde22e58..b46003e9 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictScopeStoreResolver(monitor, provider); + var resolver = new OpenIddictScopeStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs index baf0ffba..973eca85 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs @@ -26,9 +26,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests var services = new ServiceCollection(); services.AddSingleton(Mock.Of>()); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); @@ -40,9 +40,9 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>(); + var options = Mock.Of>(); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -61,14 +61,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests // Arrange var services = new ServiceCollection(); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = null }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert var exception = Assert.Throws(() => resolver.Get()); @@ -88,14 +88,14 @@ namespace OpenIddict.EntityFrameworkCore.Tests services.AddSingleton(Mock.Of>()); services.AddSingleton(CreateStore()); - var monitor = Mock.Of>( + var options = Mock.Of>( mock => mock.CurrentValue == new OpenIddictEntityFrameworkCoreOptions { DbContextType = typeof(DbContext) }); var provider = services.BuildServiceProvider(); - var resolver = new OpenIddictTokenStoreResolver(monitor, provider); + var resolver = new OpenIddictTokenStoreResolver(options, provider); // Act and assert Assert.NotNull(resolver.Get()); diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs index ac84a99d..8e536f4d 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs @@ -196,6 +196,23 @@ namespace OpenIddict.MongoDb.Tests Assert.Equal("custom_collection", options.ScopesCollectionName); } + [Fact] + public void SetInitializationTimeout_TimeoutIsCorrectlySet() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetInitializationTimeout(TimeSpan.FromDays(42)); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(TimeSpan.FromDays(42), options.InitializationTimeout); + } + [Theory] [InlineData(null)] [InlineData("")] diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs new file mode 100644 index 00000000..5edc54ad --- /dev/null +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs @@ -0,0 +1,261 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using Moq; +using OpenIddict.MongoDb.Models; +using Xunit; + +namespace OpenIddict.MongoDb.Tests +{ + public class OpenIddictMongoDbContextTests + { + [Fact] + public async Task GetDatabaseAsync_ThrowsAnExceptionForNullOptions() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var database = GetDatabase(); + var options = Mock.Of>(); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(async delegate + { + await context.GetDatabaseAsync(CancellationToken.None); + }); + + Assert.Equal("The OpenIddict MongoDB options cannot be retrieved.", exception.Message); + } + + [Fact] + public async Task GetDatabaseAsync_ThrowsAnExceptionForConcurrentCallsWhenInitializationTimesOut() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var manager = new Mock>(); + manager.Setup(mock => mock.CreateOneAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(async delegate + { + await Task.Delay(TimeSpan.FromMilliseconds(1000)); + return nameof(OpenIddictMongoDbContextTests); + }); + + var collection = new Mock>(); + collection.SetupGet(mock => mock.Indexes) + .Returns(manager.Object); + + var database = GetDatabase(); + database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) + .Returns(collection.Object); + + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictMongoDbOptions + { + Database = database.Object, + InitializationTimeout = TimeSpan.FromMilliseconds(50) + }); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(async delegate + { + await Task.WhenAll( + context.GetDatabaseAsync(CancellationToken.None).AsTask(), + context.GetDatabaseAsync(CancellationToken.None).AsTask(), + context.GetDatabaseAsync(CancellationToken.None).AsTask(), + context.GetDatabaseAsync(CancellationToken.None).AsTask()); + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The MongoDB database couldn't be initialized within a reasonable timeframe.") + .Append("Make sure that the MongoDB server is ready and accepts connections from this machine ") + .Append("or use 'options.UseMongoDb().SetInitializationTimeout()' to manually adjust the timeout.") + .ToString(), exception.Message); + } + + [Fact] + public async Task GetDatabaseAsync_PrefersDatabaseRegisteredInOptionsToDatabaseRegisteredInDependencyInjectionContainer() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of()); + + var provider = services.BuildServiceProvider(); + + var database = GetDatabase(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictMongoDbOptions + { + Database = database.Object + }); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); + } + + [Fact] + public async Task GetDatabaseAsync_ThrowsAnExceptionWhenDatabaseCannotBeFound() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var database = GetDatabase(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictMongoDbOptions + { + Database = null + }); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(async delegate + { + await context.GetDatabaseAsync(CancellationToken.None); + }); + + Assert.Equal(new StringBuilder() + .AppendLine("No suitable MongoDB database service can be found.") + .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ") + .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ") + .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.") + .ToString(), exception.Message); + } + + [Fact] + public async Task GetDatabaseAsync_UsesDatabaseRegisteredInDependencyInjectionContainer() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(Mock.Of()); + + var database = GetDatabase(); + services.AddSingleton(database.Object); + + var provider = services.BuildServiceProvider(); + + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictMongoDbOptions + { + Database = null + }); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); + } + + [Fact] + public async Task GetDatabaseAsync_ReturnsCachedDatabase() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var database = GetDatabase(); + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictMongoDbOptions + { + Database = database.Object + }); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); + Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); + + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetDatabaseAsync_FailedInvocationDoesNotPreventFutureInvocations() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var count = 0; + + var collection = new Mock>(); + collection.SetupGet(mock => mock.Indexes) + .Returns(Mock.Of>()); + + var database = GetDatabase(); + database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) + .Callback(() => count++) + .Returns(delegate + { + if (count == 1) + { + throw new Exception(); + } + + return collection.Object; + }); + + var options = Mock.Of>( + mock => mock.CurrentValue == new OpenIddictMongoDbOptions + { + Database = database.Object + }); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + await Assert.ThrowsAsync(async () => await context.GetDatabaseAsync(CancellationToken.None)); + Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); + + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Exactly(2)); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); + } + + private static Mock GetDatabase() + { + var applications = new Mock>(); + applications.SetupGet(mock => mock.Indexes) + .Returns(Mock.Of>()); + + var scopes = new Mock>(); + scopes.SetupGet(mock => mock.Indexes) + .Returns(Mock.Of>()); + + var tokens = new Mock>(); + tokens.SetupGet(mock => mock.Indexes) + .Returns(Mock.Of>()); + + var database = new Mock(); + database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) + .Returns(applications.Object); + database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) + .Returns(scopes.Object); + database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) + .Returns(tokens.Object); + + return database; + } + } +}