115 lines
4.6 KiB
C#
115 lines
4.6 KiB
C#
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);
|
|
}
|
|
}
|