refinement phase 7
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
using Baya.Application.Contracts.Payments;
|
||||
using Baya.Infrastructure.Persistence.Services.Scheduling;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Baya.Test.Foundation.Scheduling;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestration tests for <c>RecurringJobSchedulerHostedService</c> (refinement-phase-7): it runs each job at
|
||||
/// startup under the distributed lock, keeps sibling loops alive when one job throws, and stays dormant under the
|
||||
/// Testing environment so integration tests are deterministic.
|
||||
/// </summary>
|
||||
public sealed class RecurringJobSchedulerTests
|
||||
{
|
||||
private sealed class FakeEnvironment(string environmentName) : IHostEnvironment
|
||||
{
|
||||
public string EnvironmentName { get; set; } = environmentName;
|
||||
public string ApplicationName { get; set; } = "Baya.Test";
|
||||
public string ContentRootPath { get; set; } = ".";
|
||||
public Microsoft.Extensions.FileProviders.IFileProvider ContentRootFileProvider { get; set; } = null!;
|
||||
}
|
||||
|
||||
private sealed class CountingJob(string name, bool throws = false) : IRecurringJob
|
||||
{
|
||||
private int _runCount;
|
||||
public int RunCount => Volatile.Read(ref _runCount);
|
||||
public string Name => name;
|
||||
|
||||
// Long interval: run once at startup, then idle for the whole test window.
|
||||
public ValueTask<TimeSpan> GetIntervalAsync(IServiceProvider services, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(TimeSpan.FromHours(1));
|
||||
|
||||
public ValueTask RunAsync(IServiceProvider services, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _runCount);
|
||||
if (throws)
|
||||
throw new InvalidOperationException("boom");
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private static (IServiceScopeFactory scopeFactory, IDistributedLock @lock) Fakes(out IDistributedLock recordingLock)
|
||||
{
|
||||
var scopeFactory = new ServiceCollection().BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
|
||||
recordingLock = Substitute.For<IDistributedLock>();
|
||||
recordingLock.AcquireAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ValueTask<IAsyncDisposable>(Substitute.For<IAsyncDisposable>()));
|
||||
return (scopeFactory, recordingLock);
|
||||
}
|
||||
|
||||
private static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (condition())
|
||||
return;
|
||||
await Task.Delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunsJobAtStartup_UnderPerJobLock()
|
||||
{
|
||||
var (scopeFactory, @lock) = Fakes(out var recordingLock);
|
||||
var job = new CountingJob("fake_job");
|
||||
var scheduler = new RecurringJobSchedulerHostedService(
|
||||
scopeFactory, [job], @lock, new FakeEnvironment("Development"),
|
||||
NullLogger<RecurringJobSchedulerHostedService>.Instance);
|
||||
|
||||
await scheduler.StartAsync(default);
|
||||
await WaitUntilAsync(() => job.RunCount >= 1, TimeSpan.FromSeconds(3));
|
||||
await scheduler.StopAsync(default);
|
||||
|
||||
Assert.True(job.RunCount >= 1);
|
||||
await recordingLock.Received().AcquireAsync("scheduler:fake_job", Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OneJobThrowing_DoesNotStopSiblingJobs()
|
||||
{
|
||||
var (scopeFactory, @lock) = Fakes(out _);
|
||||
var throwing = new CountingJob("throwing", throws: true);
|
||||
var healthy = new CountingJob("healthy");
|
||||
var scheduler = new RecurringJobSchedulerHostedService(
|
||||
scopeFactory, [throwing, healthy], @lock, new FakeEnvironment("Development"),
|
||||
NullLogger<RecurringJobSchedulerHostedService>.Instance);
|
||||
|
||||
await scheduler.StartAsync(default);
|
||||
await WaitUntilAsync(() => healthy.RunCount >= 1, TimeSpan.FromSeconds(3));
|
||||
await scheduler.StopAsync(default);
|
||||
|
||||
Assert.True(throwing.RunCount >= 1);
|
||||
Assert.True(healthy.RunCount >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestingEnvironment_KeepsSchedulerDormant()
|
||||
{
|
||||
var (scopeFactory, @lock) = Fakes(out _);
|
||||
var job = new CountingJob("fake_job");
|
||||
var scheduler = new RecurringJobSchedulerHostedService(
|
||||
scopeFactory, [job], @lock, new FakeEnvironment("Testing"),
|
||||
NullLogger<RecurringJobSchedulerHostedService>.Instance);
|
||||
|
||||
await scheduler.StartAsync(default);
|
||||
await Task.Delay(200);
|
||||
await scheduler.StopAsync(default);
|
||||
|
||||
Assert.Equal(0, job.RunCount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user