Browse Source

Backport the MongoDB changes to OpenIddict 1.x

pull/670/head
Kévin Chalet 8 years ago
parent
commit
dd401ffb8f
  1. 9
      src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
  2. 97
      src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
  3. 7
      src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
  4. 17
      test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbBuilderTests.cs
  5. 255
      test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs

9
src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs

@ -135,6 +135,15 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.AuthorizationsCollectionName = name);
}
/// <summary>
/// Sets the maximal duration given to the MongoDB client to initialize
/// the database and register the indexes used by the OpenIddict entities.
/// </summary>
/// <param name="timeout">The timeout.</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder SetInitializationTimeout(TimeSpan timeout)
=> Configure(options => options.InitializationTimeout = timeout);
/// <summary>
/// Replaces the default scopes collection name (by default, openiddict.scopes).
/// </summary>

97
src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs

@ -23,6 +23,7 @@ namespace OpenIddict.MongoDb
{
private readonly IOptions<OpenIddictMongoDbOptions> _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);
}
/// <summary>
@ -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<IMongoDatabase>();
}
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<OpenIddictApplication>(options.ApplicationsCollectionName);
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.ClientId),
new CreateIndexOptions
try
{
var database = options.Database;
if (database == null)
{
Unique = true
});
database = _provider.GetService<IMongoDatabase>();
}
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.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<OpenIddictApplication>.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<OpenIddictApplication>(options.ApplicationsCollectionName);
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.ClientId),
new CreateIndexOptions
{
Unique = true
});
var scopes = database.GetCollection<OpenIddictScope>(options.ScopesCollectionName);
await scopes.Indexes.CreateOneAsync(
Builders<OpenIddictScope>.IndexKeys.Ascending(scope => scope.Name),
new CreateIndexOptions
{
Unique = true
});
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.PostLogoutRedirectUris));
var tokens = database.GetCollection<OpenIddictToken>(options.TokensCollectionName);
await tokens.Indexes.CreateOneAsync(
Builders<OpenIddictToken>.IndexKeys.Ascending(token => token.ReferenceId),
new CreateIndexOptions<OpenIddictToken>
{
PartialFilterExpression = Builders<OpenIddictToken>.Filter.Exists(token => token.ReferenceId),
Unique = true
});
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.RedirectUris));
return _database = database;
var scopes = database.GetCollection<OpenIddictScope>(options.ScopesCollectionName);
await scopes.Indexes.CreateOneAsync(
Builders<OpenIddictScope>.IndexKeys.Ascending(scope => scope.Name),
new CreateIndexOptions
{
Unique = true
});
var tokens = database.GetCollection<OpenIddictToken>(options.TokensCollectionName);
await tokens.Indexes.CreateOneAsync(
Builders<OpenIddictToken>.IndexKeys.Ascending(token => token.ReferenceId),
new CreateIndexOptions<OpenIddictToken>
{
PartialFilterExpression = Builders<OpenIddictToken>.Filter.Exists(token => token.ReferenceId),
Unique = true
});
return _database = database;
}
finally
{
_semaphore.Release();
}
}
return new ValueTask<IMongoDatabase>(ExecuteAsync());

7
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
/// </summary>
public IMongoDatabase Database { get; set; }
/// <summary>
/// Gets or sets the maximal duration given to the MongoDB client to initialize
/// the database and register the indexes used by the OpenIddict entities.
/// </summary>
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Gets or sets the name of the scopes collection (by default, openiddict.scopes).
/// </summary>

17
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<IOptions<OpenIddictMongoDbOptions>>().Value;
Assert.Equal(TimeSpan.FromDays(42), options.InitializationTimeout);
}
[Theory]
[InlineData(null)]
[InlineData("")]

255
test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs

@ -0,0 +1,255 @@
/*
* 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<IOptions<OpenIddictMongoDbOptions>>();
var context = new OpenIddictMongoDbContext(options, provider);
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(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<IMongoIndexManager<OpenIddictApplication>>();
manager.Setup(mock => mock.CreateOneAsync(It.IsAny<IndexKeysDefinition<OpenIddictApplication>>(), It.IsAny<CreateIndexOptions>(), It.IsAny<CancellationToken>()))
.Returns(async delegate
{
await Task.Delay(TimeSpan.FromMilliseconds(1000));
return nameof(OpenIddictMongoDbContextTests);
});
var collection = new Mock<IMongoCollection<OpenIddictApplication>>();
collection.SetupGet(mock => mock.Indexes)
.Returns(manager.Object);
var database = GetDatabase();
database.Setup(mock => mock.GetCollection<OpenIddictApplication>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Returns(collection.Object);
var options = Options.Create(new OpenIddictMongoDbOptions
{
Database = database.Object,
InitializationTimeout = TimeSpan.FromMilliseconds(50)
});
var context = new OpenIddictMongoDbContext(options, provider);
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(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<IMongoDatabase>());
var provider = services.BuildServiceProvider();
var database = GetDatabase();
var options = Options.Create(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 = Options.Create(new OpenIddictMongoDbOptions
{
Database = null
});
var context = new OpenIddictMongoDbContext(options, provider);
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(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<IMongoDatabase>());
var database = GetDatabase();
services.AddSingleton(database.Object);
var provider = services.BuildServiceProvider();
var options = Options.Create(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 = Options.Create(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<OpenIddictApplication>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()), Times.Once());
database.Verify(mock => mock.GetCollection<OpenIddictScope>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()), Times.Once());
database.Verify(mock => mock.GetCollection<OpenIddictToken>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()), Times.Once());
}
[Fact]
public async Task GetDatabaseAsync_FailedInvocationDoesNotPreventFutureInvocations()
{
// Arrange
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var count = 0;
var collection = new Mock<IMongoCollection<OpenIddictApplication>>();
collection.SetupGet(mock => mock.Indexes)
.Returns(Mock.Of<IMongoIndexManager<OpenIddictApplication>>());
var database = GetDatabase();
database.Setup(mock => mock.GetCollection<OpenIddictApplication>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Callback(() => count++)
.Returns(delegate
{
if (count == 1)
{
throw new Exception();
}
return collection.Object;
});
var options = Options.Create(new OpenIddictMongoDbOptions
{
Database = database.Object
});
var context = new OpenIddictMongoDbContext(options, provider);
// Act and assert
await Assert.ThrowsAsync<Exception>(async () => await context.GetDatabaseAsync(CancellationToken.None));
Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None));
database.Verify(mock => mock.GetCollection<OpenIddictApplication>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()), Times.Exactly(2));
database.Verify(mock => mock.GetCollection<OpenIddictScope>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()), Times.Once());
database.Verify(mock => mock.GetCollection<OpenIddictToken>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()), Times.Once());
}
private static Mock<IMongoDatabase> GetDatabase()
{
var applications = new Mock<IMongoCollection<OpenIddictApplication>>();
applications.SetupGet(mock => mock.Indexes)
.Returns(Mock.Of<IMongoIndexManager<OpenIddictApplication>>());
var scopes = new Mock<IMongoCollection<OpenIddictScope>>();
scopes.SetupGet(mock => mock.Indexes)
.Returns(Mock.Of<IMongoIndexManager<OpenIddictScope>>());
var tokens = new Mock<IMongoCollection<OpenIddictToken>>();
tokens.SetupGet(mock => mock.Indexes)
.Returns(Mock.Of<IMongoIndexManager<OpenIddictToken>>());
var database = new Mock<IMongoDatabase>();
database.Setup(mock => mock.GetCollection<OpenIddictApplication>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Returns(applications.Object);
database.Setup(mock => mock.GetCollection<OpenIddictScope>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Returns(scopes.Object);
database.Setup(mock => mock.GetCollection<OpenIddictToken>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>()))
.Returns(tokens.Object);
return database;
}
}
}
Loading…
Cancel
Save