diff --git a/Directory.Packages.props b/Directory.Packages.props index 532287a7..40909ed4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -48,7 +48,8 @@ - + + @@ -92,7 +93,7 @@ - + @@ -126,7 +127,7 @@ - + @@ -154,7 +155,6 @@ - diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj b/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj index bf3c88ad..fd450a23 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj @@ -16,7 +16,6 @@ - diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj index 27545421..011b2a3f 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj @@ -13,7 +13,6 @@ - diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj index 5ca4a091..057c0336 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj @@ -16,7 +16,6 @@ - diff --git a/src/OpenIddict.Quartz/OpenIddict.Quartz.csproj b/src/OpenIddict.Quartz/OpenIddict.Quartz.csproj index 550900d5..8cd60427 100644 --- a/src/OpenIddict.Quartz/OpenIddict.Quartz.csproj +++ b/src/OpenIddict.Quartz/OpenIddict.Quartz.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs b/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs index e71b7fc7..e6d87cd2 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzConfiguration.cs @@ -14,7 +14,7 @@ namespace OpenIddict.Quartz; /// Contains the methods required to ensure that the OpenIddict Quartz.NET configuration is valid. /// [EditorBrowsable(EditorBrowsableState.Advanced)] -public sealed class OpenIddictQuartzConfiguration : IConfigureOptions, IPostConfigureOptions +public sealed class OpenIddictQuartzConfiguration : IPostConfigureOptions { private readonly IServiceProvider _provider; @@ -25,31 +25,6 @@ public sealed class OpenIddictQuartzConfiguration : IConfigureOptions _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - /// - public void Configure(QuartzOptions options) - { - ArgumentNullException.ThrowIfNull(options); - - options.AddJob(builder => - { - builder.StoreDurably() - .WithIdentity(OpenIddictQuartzJob.Identity) - .WithDescription(SR.GetResourceString(SR.ID8001)); - }); - - options.AddTrigger(builder => - { - // 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 - // between 1 and 10 minutes after the application starts to ensure the job is executed at least once. - builder.ForJob(OpenIddictQuartzJob.Identity) - .WithIdentity(SR.GetResourceString(SR.ID8004), SR.GetResourceString(SR.ID8005)) - .WithSimpleSchedule(options => options.WithIntervalInHours(1).RepeatForever()) - .WithDescription(SR.GetResourceString(SR.ID8002)) - .StartAt(DateBuilder.FutureDate(new Random().Next(1, 10), IntervalUnit.Minute)); - }); - } - /// public void PostConfigure(string? name, OpenIddictQuartzOptions options) { diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs b/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs index 628cd50b..8a35970d 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzExtensions.cs @@ -25,16 +25,38 @@ public static class OpenIddictQuartzExtensions { ArgumentNullException.ThrowIfNull(builder); - builder.Services.AddQuartz(); + // Note: the AddQuartz() method MUST only be called once to avoid adding multiple jobs and triggers. + if (!builder.Services.Any(static descriptor => descriptor.ServiceType == typeof(OpenIddictQuartzJob))) + { + builder.Services.AddQuartz(options => + { + options.AddJob(static builder => + { + builder.StoreDurably() + .WithIdentity(OpenIddictQuartzJob.Identity) + .WithDescription(SR.GetResourceString(SR.ID8001)); + }); - // The OpenIddict job is registered as a service to allow - // Quartz.NET's DI integration to resolve it from the DI. - builder.Services.TryAddTransient(); + options.AddTrigger(static builder => + { + // 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 + // between 1 and 10 minutes after the application starts to ensure the job is executed at least once. + builder.ForJob(OpenIddictQuartzJob.Identity) + .WithIdentity(SR.GetResourceString(SR.ID8004), SR.GetResourceString(SR.ID8005)) + .WithSimpleSchedule(options => options.WithInterval(TimeSpan.FromHours(1)).RepeatForever()) + .WithDescription(SR.GetResourceString(SR.ID8002)) + .StartAt(TimeProvider.System.GetUtcNow() + TimeSpan.FromMinutes(new Random().Next(1, 10))); + }); + }); + } - // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< - IConfigureOptions, OpenIddictQuartzConfiguration>()); +#if !NET + // Note: unlike Quartz.NET 4.0+, Quartz.NET 3.x doesn't automatically register the job as a service. + builder.Services.TryAddTransient(); +#endif + // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< IPostConfigureOptions, OpenIddictQuartzConfiguration>()); diff --git a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs index 4b4277fb..c9e305ac 100644 --- a/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs +++ b/src/OpenIddict.Quartz/OpenIddictQuartzJob.cs @@ -16,24 +16,21 @@ namespace OpenIddict.Quartz; [DisallowConcurrentExecution, EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictQuartzJob : IJob { - private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; +#if !NET /// /// Creates a new instance of the class. /// public OpenIddictQuartzJob() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0082)); +#endif /// /// Creates a new instance of the class. /// - /// The OpenIddict Quartz.NET options. /// The service provider. - public OpenIddictQuartzJob(IOptionsMonitor options, IServiceProvider provider) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } + public OpenIddictQuartzJob(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// /// Gets the default identity assigned to this job. @@ -43,20 +40,24 @@ public sealed class OpenIddictQuartzJob : IJob group: SR.GetResourceString(SR.ID8005)); /// +#if NET + public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default) +#else public async Task Execute(IJobExecutionContext context) +#endif { ArgumentNullException.ThrowIfNull(context); List? 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. await using var scope = _provider.CreateAsyncScope(); + var options = scope.ServiceProvider.GetRequiredService>().CurrentValue; + // Important: since authorizations that still have tokens attached are never // pruned, the tokens MUST be deleted before deleting the authorizations. - if (!_options.CurrentValue.DisableTokenPruning) + if (!options.DisableTokenPruning) { var manager = scope.ServiceProvider.GetService() ?? throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID0278))) @@ -66,7 +67,7 @@ public sealed class OpenIddictQuartzJob : IJob UnscheduleFiringTrigger = true }; - var threshold = _options.CurrentValue.TimeProvider.GetUtcNow() - _options.CurrentValue.MinimumTokenLifespan; + var threshold = options.TimeProvider.GetUtcNow() - options.MinimumTokenLifespan; try { @@ -101,7 +102,7 @@ public sealed class OpenIddictQuartzJob : IJob } } - if (!_options.CurrentValue.DisableAuthorizationPruning) + if (!options.DisableAuthorizationPruning) { var manager = scope.ServiceProvider.GetService() ?? throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID0278))) @@ -111,7 +112,7 @@ public sealed class OpenIddictQuartzJob : IJob UnscheduleFiringTrigger = true }; - var threshold = _options.CurrentValue.TimeProvider.GetUtcNow() - _options.CurrentValue.MinimumAuthorizationLifespan; + var threshold = options.TimeProvider.GetUtcNow() - options.MinimumAuthorizationLifespan; try { @@ -149,7 +150,7 @@ public sealed class OpenIddictQuartzJob : IJob // Important: since sessions that still have tokens attached are never // pruned, the tokens MUST be deleted before deleting the sessions. - if (!_options.CurrentValue.DisableSessionPruning) + if (!options.DisableSessionPruning) { var manager = scope.ServiceProvider.GetService() ?? throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID0278))) @@ -159,7 +160,7 @@ public sealed class OpenIddictQuartzJob : IJob UnscheduleFiringTrigger = true }; - var threshold = _options.CurrentValue.TimeProvider.GetUtcNow() - _options.CurrentValue.MinimumSessionLifespan; + var threshold = options.TimeProvider.GetUtcNow() - options.MinimumSessionLifespan; try { @@ -199,7 +200,7 @@ public sealed class OpenIddictQuartzJob : IJob 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 + RefireImmediately = context.RefireCount < options.MaximumRefireCount }; } } diff --git a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs deleted file mode 100644 index 9f654c4c..00000000 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzConfigurationTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Quartz; -using Xunit; - -namespace OpenIddict.Quartz.Tests; - -public class OpenIddictQuartzConfigurationTests -{ - [Fact] - public void UseQuartz_RegistersJobDetails() - { - // Arrange - var options = new QuartzOptions(); - var configuration = new OpenIddictQuartzConfiguration(new ServiceCollection().BuildServiceProvider()); - - // Act - configuration.Configure(options); - - // Assert - Assert.Single(options.JobDetails); - Assert.Equal(OpenIddictQuartzJob.Identity, options.JobDetails[0].Key); - Assert.Equal(SR.GetResourceString(SR.ID8003), options.JobDetails[0].Key.Name); - Assert.Equal(SR.GetResourceString(SR.ID8005), options.JobDetails[0].Key.Group); - Assert.Equal(SR.GetResourceString(SR.ID8001), options.JobDetails[0].Description); - } - - [Fact] - public void UseQuartz_RegistersTriggerDetails() - { - // Arrange - var options = new QuartzOptions(); - var configuration = new OpenIddictQuartzConfiguration(new ServiceCollection().BuildServiceProvider()); - - // Act - configuration.Configure(options); - - // Assert - Assert.Single(options.Triggers); - Assert.Equal(OpenIddictQuartzJob.Identity, options.Triggers[0].JobKey); - Assert.Equal(SR.GetResourceString(SR.ID8004), options.Triggers[0].Key.Name); - Assert.Equal(SR.GetResourceString(SR.ID8005), options.Triggers[0].Key.Group); - Assert.Equal(SR.GetResourceString(SR.ID8002), options.Triggers[0].Description); - } -} diff --git a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzExtensionsTests.cs b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzExtensionsTests.cs index 41e0b540..cbf5b173 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzExtensionsTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzExtensionsTests.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Quartz; using Xunit; namespace OpenIddict.Quartz.Tests; @@ -43,9 +42,15 @@ public class OpenIddictQuartzExtensionsTests builder.UseQuartz(); // Assert +#if NET + Assert.Contains(services, service => service.ServiceType == typeof(OpenIddictQuartzJob) && + service.ImplementationType == typeof(OpenIddictQuartzJob) && + service.Lifetime is ServiceLifetime.Scoped); +#else Assert.Contains(services, service => service.ServiceType == typeof(OpenIddictQuartzJob) && service.ImplementationType == typeof(OpenIddictQuartzJob) && service.Lifetime is ServiceLifetime.Transient); +#endif } [Fact] @@ -62,11 +67,9 @@ public class OpenIddictQuartzExtensionsTests // Assert Assert.Single(services, service => service.ServiceType == typeof(OpenIddictQuartzJob) && - service.ImplementationType == typeof(OpenIddictQuartzJob) && - service.Lifetime is ServiceLifetime.Transient); + service.ImplementationType == typeof(OpenIddictQuartzJob)); - Assert.Single(services, service => service.ServiceType == typeof(IConfigureOptions) && - service.ImplementationType == typeof(OpenIddictQuartzConfiguration) && - service.Lifetime is ServiceLifetime.Singleton); + Assert.Single(services, service => service.ServiceType == typeof(IPostConfigureOptions) && + service.ImplementationType == typeof(OpenIddictQuartzConfiguration)); } } diff --git a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs index da5bbae2..7c93830b 100644 --- a/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs +++ b/test/OpenIddict.Quartz.Tests/OpenIddictQuartzJobTests.cs @@ -8,34 +8,37 @@ namespace OpenIddict.Quartz.Tests; public class OpenIddictQuartzJobTests { +#if !NET [Fact] - public void Constructor_ThrowsAnException() + public void Constructor_ThrowsAnExceptionWhenServiceProviderCannotBeResolved() { // Arrange, act and assert var exception = Assert.Throws(() => new OpenIddictQuartzJob()); Assert.Equal(SR.GetResourceString(SR.ID0082), exception.Message); } +#endif [Fact] public async Task Execute_UsesServiceScope() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var scope = Mock.Of(scope => scope.ServiceProvider == provider); var factory = Mock.Of(factory => factory.CreateScope() == scope); - var monitor = Mock.Of>( - monitor => monitor.CurrentValue == new OpenIddictQuartzOptions - { - TimeProvider = TimeProvider.System - }); - var job = new OpenIddictQuartzJob(monitor, - Mock.Of(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory)); + var job = new OpenIddictQuartzJob(Mock.Of(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory)); // Act await job.Execute(Mock.Of()); @@ -48,15 +51,22 @@ public class OpenIddictQuartzJobTests public async Task Execute_IgnoresPruningWhenTokenPruningIsDisabled() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + DisableTokenPruning = true, + TimeProvider = TimeProvider.System + }); var manager = new Mock(); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object && + provider.GetService(typeof(IOptionsMonitor)) == monitor); - var job = CreateJob(provider, options => options.DisableTokenPruning = true); + var job = CreateJob(provider); // Act await job.Execute(Mock.Of()); @@ -69,15 +79,22 @@ public class OpenIddictQuartzJobTests public async Task Execute_IgnoresPruningWhenAuthorizationPruningIsDisabled() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + DisableAuthorizationPruning = true, + TimeProvider = TimeProvider.System + }); var manager = new Mock(); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && + provider.GetService(typeof(IOptionsMonitor)) == monitor); - var job = CreateJob(provider, options => options.DisableAuthorizationPruning = true); + var job = CreateJob(provider); // Act await job.Execute(Mock.Of()); @@ -90,15 +107,22 @@ public class OpenIddictQuartzJobTests public async Task Execute_IgnoresPruningWhenSessionPruningIsDisabled() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + DisableSessionPruning = true, + TimeProvider = TimeProvider.System + }); var manager = new Mock(); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && + provider.GetService(typeof(IOptionsMonitor)) == monitor); - var job = CreateJob(provider, options => options.DisableSessionPruning = true); + var job = CreateJob(provider); // Act await job.Execute(Mock.Of()); @@ -111,15 +135,23 @@ public class OpenIddictQuartzJobTests public async Task Execute_UnschedulesTriggersWhenTokenManagerIsMissing() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == null); + provider.GetService(typeof(IOpenIddictTokenManager)) == null && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + var exception = await Assert.ThrowsAsync(async () => + await job.Execute(Mock.Of())); Assert.False(exception.RefireImmediately); Assert.True(exception.UnscheduleAllTriggers); @@ -133,13 +165,21 @@ public class OpenIddictQuartzJobTests public async Task Execute_UnschedulesTriggersWhenAuthorizationManagerIsMissing() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictAuthorizationManager)) == null); + provider.GetService(typeof(IOpenIddictAuthorizationManager)) == null && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + var exception = await Assert.ThrowsAsync(async () => + await job.Execute(Mock.Of())); Assert.False(exception.RefireImmediately); Assert.True(exception.UnscheduleAllTriggers); @@ -153,13 +193,21 @@ public class OpenIddictQuartzJobTests public async Task Execute_UnschedulesTriggersWhenSessionManagerIsMissing() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var provider = Mock.Of(provider => - provider.GetService(typeof(IOpenIddictSessionManager)) == null); + provider.GetService(typeof(IOpenIddictSessionManager)) == null && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + var exception = await Assert.ThrowsAsync(async () => + await job.Execute(Mock.Of())); Assert.False(exception.RefireImmediately); Assert.True(exception.UnscheduleAllTriggers); @@ -173,42 +221,62 @@ public class OpenIddictQuartzJobTests public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringTokenPruning() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OutOfMemoryException()); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var job = CreateJob(provider); // Act and assert - await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + await Assert.ThrowsAsync(async () => await job.Execute(Mock.Of())); } [Fact] public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringAuthorizationPruning() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OutOfMemoryException()); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var job = CreateJob(provider); // Act and assert - await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + await Assert.ThrowsAsync(async () => await job.Execute(Mock.Of())); } [Fact] public async Task Execute_RethrowsOutOfMemoryExceptionsThrownDuringSessionPruning() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OutOfMemoryException()); @@ -216,12 +284,13 @@ public class OpenIddictQuartzJobTests var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var job = CreateJob(provider); // Act and assert - await Assert.ThrowsAsync(() => job.Execute(Mock.Of())); + await Assert.ThrowsAsync(async () => await job.Execute(Mock.Of())); } [Fact] @@ -230,20 +299,27 @@ public class OpenIddictQuartzJobTests // Arrange var token = new CancellationToken(canceled: true); + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OperationCanceledException(token)); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictTokenManager)) == manager.Object && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var context = Mock.Of(context => context.CancellationToken == token); var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + var exception = await Assert.ThrowsAsync(async () => await job.Execute(context)); Assert.False(exception.RefireImmediately); @@ -256,20 +332,27 @@ public class OpenIddictQuartzJobTests // Arrange var token = new CancellationToken(canceled: true); + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OperationCanceledException(token)); var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var context = Mock.Of(context => context.CancellationToken == token); var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + var exception = await Assert.ThrowsAsync(async () => await job.Execute(context)); Assert.False(exception.RefireImmediately); @@ -282,6 +365,12 @@ public class OpenIddictQuartzJobTests // Arrange var token = new CancellationToken(canceled: true); + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new OperationCanceledException(token)); @@ -289,14 +378,15 @@ public class OpenIddictQuartzJobTests var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == Mock.Of() && provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object); + provider.GetService(typeof(IOpenIddictSessionManager)) == manager.Object && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var context = Mock.Of(context => context.CancellationToken == token); var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + var exception = await Assert.ThrowsAsync(async () => await job.Execute(context)); Assert.False(exception.RefireImmediately); @@ -307,6 +397,12 @@ public class OpenIddictQuartzJobTests public async Task Execute_AllowsRefiringWhenExceptionsAreThrown() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var provider = new Mock(); provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager))) .Returns(CreateAuthorizationManager(new ApplicationException())); @@ -317,12 +413,15 @@ public class OpenIddictQuartzJobTests provider.Setup(provider => provider.GetService(typeof(IOpenIddictTokenManager))) .Returns(CreateTokenManager(new ApplicationException())); + provider.Setup(provider => provider.GetService(typeof(IOptionsMonitor))) + .Returns(monitor); + var context = Mock.Of(context => context.RefireCount == 0); var job = CreateJob(provider.Object); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + var exception = await Assert.ThrowsAsync(async () => await job.Execute(context)); Assert.True(exception.RefireImmediately); Assert.IsType(exception.InnerException); @@ -363,6 +462,12 @@ public class OpenIddictQuartzJobTests public async Task Execute_AllowsRefiringWhenAggregateExceptionsAreThrown() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + TimeProvider = TimeProvider.System + }); + var provider = new Mock(); provider.Setup(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager))) .Returns(CreateAuthorizationManager(new AggregateException( @@ -376,12 +481,15 @@ public class OpenIddictQuartzJobTests .Returns(CreateTokenManager(new AggregateException( new InvalidOperationException(), new ApplicationException()))); + provider.Setup(provider => provider.GetService(typeof(IOptionsMonitor))) + .Returns(monitor); + var context = Mock.Of(context => context.RefireCount == 0); var job = CreateJob(provider.Object); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + var exception = await Assert.ThrowsAsync(async () => await job.Execute(context)); Assert.True(exception.RefireImmediately); Assert.IsType(exception.InnerException); @@ -425,6 +533,13 @@ public class OpenIddictQuartzJobTests public async Task Execute_DisallowsRefiringWhenMaximumRefireCountIsReached() { // Arrange + var monitor = Mock.Of>( + monitor => monitor.CurrentValue == new OpenIddictQuartzOptions + { + MaximumRefireCount = 5, + TimeProvider = TimeProvider.System + }); + var manager = new Mock(); manager.Setup(manager => manager.PruneAsync(It.IsAny(), It.IsAny())) .Throws(new ApplicationException()); @@ -432,32 +547,25 @@ public class OpenIddictQuartzJobTests var provider = Mock.Of(provider => provider.GetService(typeof(IOpenIddictAuthorizationManager)) == manager.Object && provider.GetService(typeof(IOpenIddictSessionManager)) == Mock.Of() && - provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of()); + provider.GetService(typeof(IOpenIddictTokenManager)) == Mock.Of() && + provider.GetService(typeof(IOptionsMonitor)) == monitor); var context = Mock.Of(context => context.RefireCount == 5); - var job = CreateJob(provider, options => options.MaximumRefireCount = 5); + var job = CreateJob(provider); // Act and assert - var exception = await Assert.ThrowsAsync(() => job.Execute(context)); + var exception = await Assert.ThrowsAsync(async () => await job.Execute(context)); Assert.False(exception.RefireImmediately); } - private static OpenIddictQuartzJob CreateJob(IServiceProvider provider, Action? configuration = null) + private static OpenIddictQuartzJob CreateJob(IServiceProvider provider) { var scope = Mock.Of(scope => scope.ServiceProvider == provider); var factory = Mock.Of(factory => factory.CreateScope() == scope); - var options = new OpenIddictQuartzOptions - { - TimeProvider = TimeProvider.System - }; - - configuration?.Invoke(options); - - var monitor = Mock.Of>(monitor => monitor.CurrentValue == options); - return new OpenIddictQuartzJob(monitor, - Mock.Of(provider => provider.GetService(typeof(IServiceScopeFactory)) == factory)); + return new OpenIddictQuartzJob(Mock.Of(provider => + provider.GetService(typeof(IServiceScopeFactory)) == factory)); } }