committed by
GitHub
17 changed files with 1123 additions and 5 deletions
@ -0,0 +1,22 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;netcoreapp2.1;netcoreapp3.1;netstandard2.0;netstandard2.1</TargetFrameworks> |
|||
<Nullable>enable</Nullable> |
|||
<IsShipping>false</IsShipping> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<Description>Quartz.NET integration package for the OpenIddict server services.</Description> |
|||
<PackageTags>$(PackageTags);quartz;server</PackageTags> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Quartz.Extensions.DependencyInjection" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,92 @@ |
|||
/* |
|||
* 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.ComponentModel; |
|||
using OpenIddict.Server.Quartz; |
|||
using SR = OpenIddict.Abstractions.OpenIddictResources; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection |
|||
{ |
|||
/// <summary>
|
|||
/// Exposes the necessary methods required to configure
|
|||
/// the OpenIddict server Quartz.NET integration.
|
|||
/// </summary>
|
|||
public class OpenIddictServerQuartzBuilder |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of <see cref="OpenIddictServerQuartzBuilder"/>.
|
|||
/// </summary>
|
|||
/// <param name="services">The services collection.</param>
|
|||
public OpenIddictServerQuartzBuilder(IServiceCollection services) |
|||
=> Services = services ?? throw new ArgumentNullException(nameof(services)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the services collection.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public IServiceCollection Services { get; } |
|||
|
|||
/// <summary>
|
|||
/// Amends the default OpenIddict server Quartz.NET configuration.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
|
|||
public OpenIddictServerQuartzBuilder Configure(Action<OpenIddictServerQuartzOptions> configuration) |
|||
{ |
|||
if (configuration == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(configuration)); |
|||
} |
|||
|
|||
Services.Configure(configuration); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Disables authorizations pruning.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
|
|||
public OpenIddictServerQuartzBuilder DisableAuthorizationsPruning() |
|||
=> Configure(options => options.DisableAuthorizationsPruning = true); |
|||
|
|||
/// <summary>
|
|||
/// Disables tokens pruning.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
|
|||
public OpenIddictServerQuartzBuilder DisableTokensPruning() |
|||
=> Configure(options => options.DisableTokensPruning = true); |
|||
|
|||
/// <summary>
|
|||
/// Sets the number of times a failed Quartz.NET job can be retried.
|
|||
/// </summary>
|
|||
/// <param name="count">The number of times a failed Quartz.NET job can be retried.</param>
|
|||
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
|
|||
public OpenIddictServerQuartzBuilder SetMaximumRefireCount(int count) |
|||
{ |
|||
if (count < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(count), SR.GetResourceString(SR.ID1278)); |
|||
} |
|||
|
|||
return Configure(options => options.MaximumRefireCount = count); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override bool Equals(object? obj) => base.Equals(obj); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override int GetHashCode() => base.GetHashCode(); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override string? ToString() => base.ToString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
/* |
|||
* 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.Linq; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using OpenIddict.Server.Quartz; |
|||
using Quartz; |
|||
using SR = OpenIddict.Abstractions.OpenIddictResources; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection |
|||
{ |
|||
/// <summary>
|
|||
/// Exposes extensions allowing to register the OpenIddict server Quartz.NET integration.
|
|||
/// </summary>
|
|||
public static class OpenIddictServerQuartzExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Registers the OpenIddict server Quartz.NET integration in the DI container.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerQuartzBuilder"/>.</returns>
|
|||
public static OpenIddictServerQuartzBuilder UseQuartz(this OpenIddictServerBuilder builder) |
|||
{ |
|||
if (builder == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(builder)); |
|||
} |
|||
|
|||
// Warning: the AddQuartz() method is deliberately not used as it's not idempotent.
|
|||
// Calling it at this point may override user-defined services (e.g Quartz DI support).
|
|||
|
|||
builder.Services.TryAddTransient<OpenIddictServerQuartzJob>(); |
|||
|
|||
// To ensure this method can be safely called multiple times, the job details
|
|||
// of the OpenIddict server job are only added if no existing IJobDetail instance
|
|||
// pointing to OpenIddictServerQuartzJob was already registered in the DI container.
|
|||
if (!builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IJobDetail) && |
|||
descriptor.ImplementationInstance is IJobDetail job && |
|||
job.Key.Equals(OpenIddictServerQuartzJob.Identity))) |
|||
{ |
|||
builder.Services.AddSingleton( |
|||
JobBuilder.Create<OpenIddictServerQuartzJob>() |
|||
.StoreDurably() |
|||
.WithIdentity(OpenIddictServerQuartzJob.Identity) |
|||
.WithDescription(SR.GetResourceString(SR.ID9000)) |
|||
.Build()); |
|||
} |
|||
|
|||
// To ensure this method can be safely called multiple times, the trigger details
|
|||
// of the OpenIddict server job are only added if no existing ITrigger instance
|
|||
// pointing to OpenIddictServerQuartzJob was already registered in the DI container.
|
|||
if (!builder.Services.Any(descriptor => descriptor.ServiceType == typeof(ITrigger) && |
|||
descriptor.ImplementationInstance is ITrigger trigger && |
|||
trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity))) |
|||
{ |
|||
// Note: this trigger uses a quite long interval (1 hour), which means it may be
|
|||
// potentially never reached if the application is shut down or recycled. As such,
|
|||
// this trigger is set up to fire immediately to ensure it's executed at least once.
|
|||
builder.Services.AddSingleton( |
|||
TriggerBuilder.Create() |
|||
.ForJob(OpenIddictServerQuartzJob.Identity) |
|||
.WithSimpleSchedule(options => options.WithIntervalInHours(1).RepeatForever()) |
|||
.WithDescription(SR.GetResourceString(SR.ID9001)) |
|||
.StartNow() |
|||
.Build()); |
|||
} |
|||
|
|||
return new OpenIddictServerQuartzBuilder(builder.Services); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers the OpenIddict server Quartz.NET integration in the DI container.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <param name="configuration">The configuration delegate used to configure the server services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
|
|||
public static OpenIddictServerBuilder UseQuartz( |
|||
this OpenIddictServerBuilder builder, Action<OpenIddictServerQuartzBuilder> configuration) |
|||
{ |
|||
if (builder == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(builder)); |
|||
} |
|||
|
|||
if (configuration == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(configuration)); |
|||
} |
|||
|
|||
configuration(builder.UseQuartz()); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
/* |
|||
* 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.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using OpenIddict.Abstractions; |
|||
using Quartz; |
|||
using SR = OpenIddict.Abstractions.OpenIddictResources; |
|||
|
|||
namespace OpenIddict.Server.Quartz |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a Quartz.NET job performing scheduled tasks for the OpenIddict server feature.
|
|||
/// </summary>
|
|||
[DisallowConcurrentExecution] |
|||
public class OpenIddictServerQuartzJob : IJob |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerQuartzOptions> _options; |
|||
private readonly IServiceProvider _provider; |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="OpenIddictServerQuartzJob"/> class.
|
|||
/// </summary>
|
|||
public OpenIddictServerQuartzJob() => throw new InvalidOperationException(SR.GetResourceString(SR.ID1081)); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="OpenIddictServerQuartzJob"/> class.
|
|||
/// </summary>
|
|||
/// <param name="options">The OpenIddict server Quartz.NET options.</param>
|
|||
/// <param name="provider">The service provider.</param>
|
|||
public OpenIddictServerQuartzJob(IOptionsMonitor<OpenIddictServerQuartzOptions> options, IServiceProvider provider) |
|||
{ |
|||
_options = options; |
|||
_provider = provider; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the default identity assigned to this job.
|
|||
/// </summary>
|
|||
public static JobKey Identity { get; } = new JobKey( |
|||
name: typeof(OpenIddictServerQuartzJob).Name, |
|||
group: typeof(OpenIddictServerQuartzJob).Assembly.GetName().Name!); |
|||
|
|||
/// <inheritdoc/>
|
|||
public async Task Execute(IJobExecutionContext context) |
|||
{ |
|||
if (context == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
List<Exception>? exceptions = null; |
|||
|
|||
// Note: this job is registered as a transient service. As such, it cannot directly depend on scoped services
|
|||
// like the core managers. To work around this limitation, a scope is manually created for each invocation.
|
|||
var scope = _provider.CreateScope(); |
|||
|
|||
try |
|||
{ |
|||
// Note: this background task is responsible of automatically removing orphaned tokens/authorizations
|
|||
// (i.e tokens that are no longer valid and ad-hoc authorizations that have no valid tokens associated).
|
|||
// Since ad-hoc authorizations and their associated tokens are removed as part of the same operation
|
|||
// when they no longer have any token attached, it's more efficient to remove the authorizations first.
|
|||
|
|||
// Note: the authorization/token managers MUST be resolved from the scoped provider
|
|||
// as they depend on scoped stores that should be disposed as soon as possible.
|
|||
|
|||
if (!_options.CurrentValue.DisableAuthorizationsPruning) |
|||
{ |
|||
var manager = scope.ServiceProvider.GetService<IOpenIddictAuthorizationManager>(); |
|||
if (manager == null) |
|||
{ |
|||
// Inform Quartz.NET that the triggers associated with this job should be removed,
|
|||
// as the future invocations will always fail until the application is correctly
|
|||
// re-configured to register the OpenIddict core services in the DI container.
|
|||
throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID1277))) |
|||
{ |
|||
RefireImmediately = false, |
|||
UnscheduleAllTriggers = true, |
|||
UnscheduleFiringTrigger = true |
|||
}; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
await manager.PruneAsync(context.CancellationToken); |
|||
} |
|||
|
|||
// OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is.
|
|||
catch (OutOfMemoryException) |
|||
{ |
|||
throw; |
|||
} |
|||
|
|||
// OperationCanceledExceptions are typically thrown the host is about to shut down.
|
|||
// To allow the host to shut down as fast as possible, this exception type is special-cased
|
|||
// to prevent further processing in this job and inform Quartz.NET it shouldn't be refired.
|
|||
catch (OperationCanceledException exception) when (exception.CancellationToken == context.CancellationToken) |
|||
{ |
|||
throw new JobExecutionException(exception) |
|||
{ |
|||
RefireImmediately = false |
|||
}; |
|||
} |
|||
|
|||
// AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s)
|
|||
// occurred while trying to prune the entities. In this case, add the inner exceptions to the collection.
|
|||
catch (AggregateException exception) |
|||
{ |
|||
exceptions ??= new List<Exception>(capacity: 1); |
|||
exceptions.AddRange(exception.InnerExceptions); |
|||
} |
|||
|
|||
// Other exceptions are assumed to be transient and are added to the exceptions collection
|
|||
// to be re-thrown later (typically, at the very end of this job, as an AggregateException).
|
|||
catch (Exception exception) |
|||
{ |
|||
exceptions ??= new List<Exception>(capacity: 1); |
|||
exceptions.Add(exception); |
|||
} |
|||
} |
|||
|
|||
if (!_options.CurrentValue.DisableTokensPruning) |
|||
{ |
|||
var manager = scope.ServiceProvider.GetService<IOpenIddictTokenManager>(); |
|||
if (manager == null) |
|||
{ |
|||
// Inform Quartz.NET that the triggers associated with this job should be removed,
|
|||
// as the future invocations will always fail until the application is correctly
|
|||
// re-configured to register the OpenIddict core services in the DI container.
|
|||
throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID1277))) |
|||
{ |
|||
RefireImmediately = false, |
|||
UnscheduleAllTriggers = true, |
|||
UnscheduleFiringTrigger = true |
|||
}; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
await manager.PruneAsync(context.CancellationToken); |
|||
} |
|||
|
|||
// OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is.
|
|||
catch (OutOfMemoryException) |
|||
{ |
|||
throw; |
|||
} |
|||
|
|||
// OperationCanceledExceptions are typically thrown the host is about to shut down.
|
|||
// To allow the host to shut down as fast as possible, this exception type is special-cased
|
|||
// to prevent further processing in this job and inform Quartz.NET it shouldn't be refired.
|
|||
catch (OperationCanceledException exception) when (exception.CancellationToken == context.CancellationToken) |
|||
{ |
|||
throw new JobExecutionException(exception) |
|||
{ |
|||
RefireImmediately = false |
|||
}; |
|||
} |
|||
|
|||
// AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s)
|
|||
// occurred while trying to prune the entities. In this case, add the inner exceptions to the collection.
|
|||
catch (AggregateException exception) |
|||
{ |
|||
exceptions ??= new List<Exception>(capacity: 1); |
|||
exceptions.AddRange(exception.InnerExceptions); |
|||
} |
|||
|
|||
// Other exceptions are assumed to be transient and are added to the exceptions collection
|
|||
// to be re-thrown later (typically, at the very end of this job, as an AggregateException).
|
|||
catch (Exception exception) |
|||
{ |
|||
exceptions ??= new List<Exception>(capacity: 1); |
|||
exceptions.Add(exception); |
|||
} |
|||
} |
|||
|
|||
if (exceptions != null) |
|||
{ |
|||
throw new JobExecutionException(new AggregateException(exceptions)) |
|||
{ |
|||
// Only refire the job if the maximum refire count set in the options wasn't reached.
|
|||
RefireImmediately = context.RefireCount < _options.CurrentValue.MaximumRefireCount |
|||
}; |
|||
} |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
if (scope is IAsyncDisposable disposable) |
|||
{ |
|||
await disposable.DisposeAsync(); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
scope.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/* |
|||
* 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. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Server.Quartz |
|||
{ |
|||
/// <summary>
|
|||
/// Provides various settings needed to configure the OpenIddict Quartz.NET server integration.
|
|||
/// </summary>
|
|||
public class OpenIddictServerQuartzOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets a boolean indicating whether authorizations pruning should be disabled.
|
|||
/// </summary>
|
|||
public bool DisableAuthorizationsPruning { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a boolean indicating whether tokens pruning should be disabled.
|
|||
/// </summary>
|
|||
public bool DisableTokensPruning { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of times a failed Quartz.NET job can be retried.
|
|||
/// By default, failed jobs are automatically retried twice after the initial failure.
|
|||
/// </summary>
|
|||
public int MaximumRefireCount { get; set; } = 2; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;netcoreapp2.1;netcoreapp3.1</TargetFrameworks> |
|||
<IsPackable>false</IsPackable> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Moq" /> |
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Server.Quartz\OpenIddict.Server.Quartz.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,127 @@ |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Xunit; |
|||
using SR = OpenIddict.Abstractions.OpenIddictResources; |
|||
|
|||
namespace OpenIddict.Server.Quartz.Tests |
|||
{ |
|||
public class OpenIddictServerQuartzBuilderTests |
|||
{ |
|||
[Fact] |
|||
public void Constructor_ThrowsAnExceptionForNullServices() |
|||
{ |
|||
// Arrange
|
|||
var services = (IServiceCollection) null!; |
|||
|
|||
// Act and assert
|
|||
var exception = Assert.Throws<ArgumentNullException>(() => new OpenIddictServerQuartzBuilder(services)); |
|||
|
|||
Assert.Equal("services", exception.ParamName); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Configure_DelegateIsCorrectlyRegistered() |
|||
{ |
|||
// Arrange
|
|||
var services = CreateServices(); |
|||
var builder = CreateBuilder(services); |
|||
var configuration = new Action<OpenIddictServerQuartzOptions>(options => { }); |
|||
|
|||
// Act
|
|||
builder.Configure(configuration); |
|||
|
|||
// Assert
|
|||
Assert.Contains(services, service => service.ServiceType == typeof(IConfigureOptions<OpenIddictServerQuartzOptions>) && |
|||
service.ImplementationInstance is ConfigureNamedOptions<OpenIddictServerQuartzOptions> options && |
|||
options.Action == configuration && string.IsNullOrEmpty(options.Name)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Configure_ThrowsAnExceptionWhenConfigurationIsNull() |
|||
{ |
|||
// Arrange
|
|||
var services = CreateServices(); |
|||
var builder = CreateBuilder(services); |
|||
|
|||
// Act and assert
|
|||
var exception = Assert.Throws<ArgumentNullException>(() => builder.Configure(configuration: null!)); |
|||
Assert.Equal("configuration", exception.ParamName); |
|||
} |
|||
|
|||
[Fact] |
|||
public void DisableAuthorizationsPruning_AuthorizationsPruningIsDisabled() |
|||
{ |
|||
// Arrange
|
|||
var services = CreateServices(); |
|||
var builder = CreateBuilder(services); |
|||
|
|||
// Act
|
|||
builder.DisableAuthorizationsPruning(); |
|||
|
|||
var options = GetOptions(services); |
|||
|
|||
// Assert
|
|||
Assert.True(options.DisableAuthorizationsPruning); |
|||
} |
|||
|
|||
[Fact] |
|||
public void DisableTokensPruning_TokensPruningIsDisabled() |
|||
{ |
|||
// Arrange
|
|||
var services = CreateServices(); |
|||
var builder = CreateBuilder(services); |
|||
|
|||
// Act
|
|||
builder.DisableTokensPruning(); |
|||
|
|||
var options = GetOptions(services); |
|||
|
|||
// Assert
|
|||
Assert.True(options.DisableTokensPruning); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetMaximumRefireCount_ThrowsAnExceptionForNegativeCount() |
|||
{ |
|||
// Arrange
|
|||
var services = CreateServices(); |
|||
var builder = CreateBuilder(services); |
|||
|
|||
// Act and assert
|
|||
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => builder.SetMaximumRefireCount(-1)); |
|||
|
|||
Assert.Equal("count", exception.ParamName); |
|||
Assert.StartsWith(SR.GetResourceString(SR.ID1278), exception.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetMaximumRefireCount_MaximumRefireCountIsSet() |
|||
{ |
|||
// Arrange
|
|||
var services = CreateServices(); |
|||
var builder = CreateBuilder(services); |
|||
|
|||
// Act
|
|||
builder.SetMaximumRefireCount(42); |
|||
|
|||
var options = GetOptions(services); |
|||
|
|||
// Assert
|
|||
Assert.Equal(42, options.MaximumRefireCount); |
|||
} |
|||
|
|||
private static IServiceCollection CreateServices() |
|||
=> new ServiceCollection().AddOptions(); |
|||
|
|||
private static OpenIddictServerQuartzBuilder CreateBuilder(IServiceCollection services) |
|||
=> new OpenIddictServerQuartzBuilder(services); |
|||
|
|||
private static OpenIddictServerQuartzOptions GetOptions(IServiceCollection services) |
|||
{ |
|||
var provider = services.BuildServiceProvider(); |
|||
var options = provider.GetRequiredService<IOptions<OpenIddictServerQuartzOptions>>(); |
|||
return options.Value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Quartz; |
|||
using Xunit; |
|||
|
|||
namespace OpenIddict.Server.Quartz.Tests |
|||
{ |
|||
public class OpenIddictServerQuartzExtensionsTests |
|||
{ |
|||
[Fact] |
|||
public void UseQuartz_ThrowsAnExceptionForNullBuilder() |
|||
{ |
|||
// Arrange
|
|||
var builder = (OpenIddictServerBuilder) null!; |
|||
|
|||
// Act and assert
|
|||
var exception = Assert.Throws<ArgumentNullException>(() => builder.UseQuartz()); |
|||
|
|||
Assert.Equal("builder", exception.ParamName); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UseQuartz_ThrowsAnExceptionForNullConfiguration() |
|||
{ |
|||
// Arrange
|
|||
var services = new ServiceCollection(); |
|||
var builder = new OpenIddictServerBuilder(services); |
|||
|
|||
// Act and assert
|
|||
var exception = Assert.Throws<ArgumentNullException>(() => builder.UseQuartz(configuration: null!)); |
|||
|
|||
Assert.Equal("configuration", exception.ParamName); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UseQuartz_RegistersJobService() |
|||
{ |
|||
// Arrange
|
|||
var services = new ServiceCollection(); |
|||
var builder = new OpenIddictServerBuilder(services); |
|||
|
|||
// Act
|
|||
builder.UseQuartz(); |
|||
|
|||
// Assert
|
|||
Assert.Contains(services, service => service.ServiceType == typeof(OpenIddictServerQuartzJob) && |
|||
service.ImplementationType == typeof(OpenIddictServerQuartzJob) && |
|||
service.Lifetime == ServiceLifetime.Transient); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UseQuartz_RegistersJobDetails() |
|||
{ |
|||
// Arrange
|
|||
var services = new ServiceCollection(); |
|||
var builder = new OpenIddictServerBuilder(services); |
|||
|
|||
// Act
|
|||
builder.UseQuartz(); |
|||
|
|||
// Assert
|
|||
Assert.Contains(services, service => service.ServiceType == typeof(IJobDetail) && |
|||
service.ImplementationInstance is IJobDetail job && |
|||
job.Key.Equals(OpenIddictServerQuartzJob.Identity)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UseQuartz_RegistersTriggerDetails() |
|||
{ |
|||
// Arrange
|
|||
var services = new ServiceCollection(); |
|||
var builder = new OpenIddictServerBuilder(services); |
|||
|
|||
// Act
|
|||
builder.UseQuartz(); |
|||
|
|||
// Assert
|
|||
Assert.Contains(services, service => service.ServiceType == typeof(ITrigger) && |
|||
service.ImplementationInstance is ITrigger trigger && |
|||
trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UseQuartz_CanBeSafelyInvokedMultipleTimes() |
|||
{ |
|||
// Arrange
|
|||
var services = new ServiceCollection(); |
|||
var builder = new OpenIddictServerBuilder(services); |
|||
|
|||
// Act
|
|||
builder.UseQuartz(); |
|||
builder.UseQuartz(); |
|||
builder.UseQuartz(); |
|||
|
|||
// Assert
|
|||
Assert.Single(services, service => service.ServiceType == typeof(IJobDetail) && |
|||
service.ImplementationInstance is IJobDetail job && |
|||
job.Key.Equals(OpenIddictServerQuartzJob.Identity)); |
|||
|
|||
Assert.Single(services, service => service.ServiceType == typeof(ITrigger) && |
|||
service.ImplementationInstance is ITrigger trigger && |
|||
trigger.JobKey.Equals(OpenIddictServerQuartzJob.Identity)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,349 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Moq; |
|||
using OpenIddict.Abstractions; |
|||
using Quartz; |
|||
using Xunit; |
|||
using SR = OpenIddict.Abstractions.OpenIddictResources; |
|||
|
|||
namespace OpenIddict.Server.Quartz.Tests |
|||
{ |
|||
public class OpenIddictServerQuartzJobTests |
|||
{ |
|||
[Fact] |
|||
public void Constructor_ThrowsAnException() |
|||
{ |
|||
// Arrange, act and assert
|
|||
var exception = Assert.Throws<InvalidOperationException>(() => new OpenIddictServerQuartzJob()); |
|||
|
|||
Assert.Equal(SR.GetResourceString(SR.ID1081), exception.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_UsesServiceScope() |
|||
{ |
|||
// Arrange
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>()); |
|||
|
|||
var scope = Mock.Of<IServiceScope>(scope => scope.ServiceProvider == provider); |
|||
var factory = Mock.Of<IServiceScopeFactory>(factory => factory.CreateScope() == scope); |
|||
var monitor = Mock.Of<IOptionsMonitor<OpenIddictServerQuartzOptions>>( |
|||
monitor => monitor.CurrentValue == new OpenIddictServerQuartzOptions()); |
|||
|
|||
var job = new OpenIddictServerQuartzJob(monitor, |
|||
Mock.Of<IServiceProvider>(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory)); |
|||
|
|||
// Act
|
|||
await job.Execute(Mock.Of<IJobExecutionContext>()); |
|||
|
|||
Mock.Get(factory).Verify(factory => factory.CreateScope(), Times.Once()); |
|||
Mock.Get(scope).Verify(scope => scope.Dispose(), Times.Once()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_IgnoresPruningWhenAuthorizationsPruningIsDisabled() |
|||
{ |
|||
// Arrange
|
|||
|
|||
var manager = new Mock<IOpenIddictAuthorizationManager>(); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>()); |
|||
|
|||
var job = CreateJob(provider, new OpenIddictServerQuartzOptions |
|||
{ |
|||
DisableAuthorizationsPruning = true |
|||
}); |
|||
|
|||
// Act
|
|||
await job.Execute(Mock.Of<IJobExecutionContext>()); |
|||
|
|||
// Assert
|
|||
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Never()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_IgnoresPruningWhenTokensPruningIsDisabled() |
|||
{ |
|||
// Arrange
|
|||
|
|||
var manager = new Mock<IOpenIddictAuthorizationManager>(); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); |
|||
|
|||
var job = CreateJob(provider, new OpenIddictServerQuartzOptions |
|||
{ |
|||
DisableTokensPruning = true |
|||
}); |
|||
|
|||
// Act
|
|||
await job.Execute(Mock.Of<IJobExecutionContext>()); |
|||
|
|||
// Assert
|
|||
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Never()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_UnschedulesTriggersWhenAuthorizationManagerIsMissing() |
|||
{ |
|||
// Arrange
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == null); |
|||
|
|||
var job = CreateJob(provider); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(Mock.Of<IJobExecutionContext>())); |
|||
|
|||
Assert.False(exception.RefireImmediately); |
|||
Assert.True(exception.UnscheduleAllTriggers); |
|||
Assert.True(exception.UnscheduleFiringTrigger); |
|||
|
|||
Assert.IsType<InvalidOperationException>(exception.InnerException); |
|||
Assert.Equal(SR.GetResourceString(SR.ID1277), exception.InnerException!.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_UnschedulesTriggersWhenTokenManagerIsMissing() |
|||
{ |
|||
// Arrange
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == null); |
|||
|
|||
var job = CreateJob(provider); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(Mock.Of<IJobExecutionContext>())); |
|||
|
|||
Assert.False(exception.RefireImmediately); |
|||
Assert.True(exception.UnscheduleAllTriggers); |
|||
Assert.True(exception.UnscheduleFiringTrigger); |
|||
|
|||
Assert.IsType<InvalidOperationException>(exception.InnerException); |
|||
Assert.Equal(SR.GetResourceString(SR.ID1277), exception.InnerException!.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringAuthorizationsPruning() |
|||
{ |
|||
// Arrange
|
|||
var manager = new Mock<IOpenIddictAuthorizationManager>(); |
|||
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(new OutOfMemoryException()); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>()); |
|||
|
|||
var job = CreateJob(provider); |
|||
|
|||
// Act and assert
|
|||
await Assert.ThrowsAsync<OutOfMemoryException>(() => job.Execute(Mock.Of<IJobExecutionContext>())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringTokensPruning() |
|||
{ |
|||
// Arrange
|
|||
var manager = new Mock<IOpenIddictTokenManager>(); |
|||
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(new OutOfMemoryException()); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); |
|||
|
|||
var job = CreateJob(provider); |
|||
|
|||
// Act and assert
|
|||
await Assert.ThrowsAsync<OutOfMemoryException>(() => job.Execute(Mock.Of<IJobExecutionContext>())); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringAuthorizationsPruning() |
|||
{ |
|||
// Arrange
|
|||
var token = new CancellationToken(canceled: true); |
|||
|
|||
var manager = new Mock<IOpenIddictAuthorizationManager>(); |
|||
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(new OperationCanceledException(token)); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>()); |
|||
|
|||
var context = Mock.Of<IJobExecutionContext>(context => context.CancellationToken == token); |
|||
|
|||
var job = CreateJob(provider); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context)); |
|||
|
|||
Assert.False(exception.RefireImmediately); |
|||
|
|||
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Once()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_DisablesRefiringWhenJobIsCanceledDuringTokensPruning() |
|||
{ |
|||
// Arrange
|
|||
var token = new CancellationToken(canceled: true); |
|||
|
|||
var manager = new Mock<IOpenIddictTokenManager>(); |
|||
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(new OperationCanceledException(token)); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of<IOpenIddictAuthorizationManager>() && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); |
|||
|
|||
var context = Mock.Of<IJobExecutionContext>(context => context.CancellationToken == token); |
|||
|
|||
var job = CreateJob(provider); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context)); |
|||
|
|||
Assert.False(exception.RefireImmediately); |
|||
|
|||
manager.Verify(manager => manager.PruneAsync(It.IsAny<CancellationToken>()), Times.Once()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_AllowsRefiringWhenExceptionsAreThrown() |
|||
{ |
|||
// Arrange
|
|||
var provider = new Mock<IServiceProvider>(); |
|||
provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager))) |
|||
.Returns(CreateAuthorizationManager(new ApplicationException())); |
|||
|
|||
provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager))) |
|||
.Returns(CreateTokenManager(new ApplicationException())); |
|||
|
|||
var context = Mock.Of<IJobExecutionContext>(context => context.RefireCount == 0); |
|||
|
|||
var job = CreateJob(provider.Object); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context)); |
|||
|
|||
Assert.True(exception.RefireImmediately); |
|||
Assert.IsType<AggregateException>(exception.InnerException); |
|||
Assert.Equal(2, ((AggregateException) exception.InnerException!).InnerExceptions.Count); |
|||
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[0]); |
|||
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[1]); |
|||
|
|||
static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception) |
|||
{ |
|||
var mock = new Mock<IOpenIddictAuthorizationManager>(); |
|||
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(exception); |
|||
|
|||
return mock.Object; |
|||
} |
|||
|
|||
static IOpenIddictTokenManager CreateTokenManager(Exception exception) |
|||
{ |
|||
var mock = new Mock<IOpenIddictTokenManager>(); |
|||
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(exception); |
|||
|
|||
return mock.Object; |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_AllowsRefiringWhenAggregateExceptionsAreThrown() |
|||
{ |
|||
// Arrange
|
|||
var provider = new Mock<IServiceProvider>(); |
|||
provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager))) |
|||
.Returns(CreateAuthorizationManager(new AggregateException( |
|||
new InvalidOperationException(), new ApplicationException()))); |
|||
|
|||
provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager))) |
|||
.Returns(CreateTokenManager(new AggregateException( |
|||
new InvalidOperationException(), new ApplicationException()))); |
|||
|
|||
var context = Mock.Of<IJobExecutionContext>(context => context.RefireCount == 0); |
|||
|
|||
var job = CreateJob(provider.Object); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context)); |
|||
|
|||
Assert.True(exception.RefireImmediately); |
|||
Assert.IsType<AggregateException>(exception.InnerException); |
|||
Assert.Equal(4, ((AggregateException) exception.InnerException!).InnerExceptions.Count); |
|||
Assert.IsType<InvalidOperationException>(((AggregateException) exception.InnerException!).InnerExceptions[0]); |
|||
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[1]); |
|||
Assert.IsType<InvalidOperationException>(((AggregateException) exception.InnerException!).InnerExceptions[2]); |
|||
Assert.IsType<ApplicationException>(((AggregateException) exception.InnerException!).InnerExceptions[3]); |
|||
|
|||
static IOpenIddictAuthorizationManager CreateAuthorizationManager(Exception exception) |
|||
{ |
|||
var mock = new Mock<IOpenIddictAuthorizationManager>(); |
|||
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(exception); |
|||
|
|||
return mock.Object; |
|||
} |
|||
|
|||
static IOpenIddictTokenManager CreateTokenManager(Exception exception) |
|||
{ |
|||
var mock = new Mock<IOpenIddictTokenManager>(); |
|||
mock.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(exception); |
|||
|
|||
return mock.Object; |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Execute_DisallowsRefiringWhenMaximumRefireCountIsReached() |
|||
{ |
|||
// Arrange
|
|||
var manager = new Mock<IOpenIddictAuthorizationManager>(); |
|||
manager.Setup(manager => manager.PruneAsync(It.IsAny<CancellationToken>())) |
|||
.Throws(new ApplicationException()); |
|||
|
|||
var provider = Mock.Of<IServiceProvider>(provider => |
|||
provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && |
|||
provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of<IOpenIddictTokenManager>()); |
|||
|
|||
var context = Mock.Of<IJobExecutionContext>(context => context.RefireCount == 5); |
|||
|
|||
var job = CreateJob(provider, new OpenIddictServerQuartzOptions |
|||
{ |
|||
MaximumRefireCount = 5 |
|||
}); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<JobExecutionException>(() => job.Execute(context)); |
|||
|
|||
Assert.False(exception.RefireImmediately); |
|||
} |
|||
|
|||
private static OpenIddictServerQuartzJob CreateJob(IServiceProvider provider, OpenIddictServerQuartzOptions? options = null) |
|||
{ |
|||
var scope = Mock.Of<IServiceScope>(scope => scope.ServiceProvider == provider); |
|||
var factory = Mock.Of<IServiceScopeFactory>(factory => factory.CreateScope() == scope); |
|||
var monitor = Mock.Of<IOptionsMonitor<OpenIddictServerQuartzOptions>>( |
|||
monitor => monitor.CurrentValue == (options ?? new OpenIddictServerQuartzOptions())); |
|||
|
|||
return new OpenIddictServerQuartzJob(monitor, |
|||
Mock.Of<IServiceProvider>(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory)); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue