mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
189 lines
6.5 KiB
C#
189 lines
6.5 KiB
C#
using Cleanuparr.Api.Hubs;
|
|
using Cleanuparr.Api.Jobs;
|
|
using Cleanuparr.Api.Tests.TestHelpers;
|
|
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Infrastructure.Features.Context;
|
|
using Cleanuparr.Infrastructure.Features.DryRun;
|
|
using Cleanuparr.Infrastructure.Features.Jobs;
|
|
using Cleanuparr.Infrastructure.Interceptors;
|
|
using Cleanuparr.Infrastructure.Models;
|
|
using Cleanuparr.Infrastructure.Services.Interfaces;
|
|
using Cleanuparr.Persistence;
|
|
using Cleanuparr.Persistence.Models.Configuration.General;
|
|
using Cleanuparr.Persistence.Models.State;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Time.Testing;
|
|
using NSubstitute;
|
|
using Quartz;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace Cleanuparr.Api.Tests.Jobs;
|
|
|
|
public sealed class GenericJobTests : IDisposable
|
|
{
|
|
private readonly EventsContext _eventsContext;
|
|
private readonly DataContext _dataContext;
|
|
private readonly DryRunActivity _dryRunActivity;
|
|
private readonly ServiceProvider _serviceProvider;
|
|
private readonly Seeker _handler = new();
|
|
|
|
public GenericJobTests()
|
|
{
|
|
_eventsContext = ConfigControllerTestDataFactory.CreateEventsContext();
|
|
_dataContext = ConfigControllerTestDataFactory.CreateDataContext();
|
|
_dryRunActivity = new DryRunActivity();
|
|
|
|
IJobManagementService jobManagementService = Substitute.For<IJobManagementService>();
|
|
jobManagementService.GetJob(Arg.Any<JobType>()).Returns(new JobInfo { JobType = nameof(JobType.Seeker) });
|
|
|
|
ServiceCollection services = new();
|
|
services.AddSingleton(_eventsContext);
|
|
services.AddSingleton(Substitute.For<IHubContext<AppHub>>());
|
|
services.AddSingleton(jobManagementService);
|
|
services.AddSingleton(_handler);
|
|
services.AddScoped<IDryRunInterceptor>(_ =>
|
|
new DryRunInterceptor(Substitute.For<ILogger<DryRunInterceptor>>(), _dataContext));
|
|
_serviceProvider = services.BuildServiceProvider();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_serviceProvider.Dispose();
|
|
_eventsContext.Dispose();
|
|
_dataContext.Dispose();
|
|
}
|
|
|
|
private static readonly DateTimeOffset Now = new(2026, 1, 1, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
private GenericJob<Seeker> BuildJob() => new(
|
|
Substitute.For<ILogger<GenericJob<Seeker>>>(),
|
|
_serviceProvider.GetRequiredService<IServiceScopeFactory>(),
|
|
new FakeTimeProvider(Now),
|
|
_dryRunActivity);
|
|
|
|
[Fact]
|
|
public async Task Execute_StampsTheRunAsCompleted()
|
|
{
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
JobRun run = await _eventsContext.JobRuns.SingleAsync();
|
|
run.Status.ShouldBe(JobRunStatus.Completed);
|
|
run.CompletedAt.ShouldNotBeNull();
|
|
run.CompletedAt!.Value.ShouldBe(Now);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_StampsTheRunWhenTheHandlerThrows()
|
|
{
|
|
_handler.Throw = new InvalidOperationException("handler exploded");
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
JobRun run = await _eventsContext.JobRuns.SingleAsync();
|
|
run.Status.ShouldBe(JobRunStatus.Failed);
|
|
run.CompletedAt!.Value.ShouldBe(Now);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_CapturesDryRunOnceForTheWholeRun()
|
|
{
|
|
GeneralConfig config = await _dataContext.GeneralConfigs.SingleAsync();
|
|
config.DryRun = true;
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
_handler.ObservedDryRun.ShouldBe(true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_DoesNotStickWhenTheRunStartedLive()
|
|
{
|
|
GeneralConfig config = await _dataContext.GeneralConfigs.SingleAsync();
|
|
config.DryRun = false;
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
_handler.ObservedDryRun.ShouldBe(false);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_WhenRunStartedDry_RequestsAPurgeOnceFinished()
|
|
{
|
|
GeneralConfig config = await _dataContext.GeneralConfigs.SingleAsync();
|
|
config.DryRun = true;
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
(await _dryRunActivity.WaitForPurgeRequestAsync(CancellationToken.None)).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_WhenRunStartedLive_RequestsNoPurge()
|
|
{
|
|
GeneralConfig config = await _dataContext.GeneralConfigs.SingleAsync();
|
|
config.DryRun = false;
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
Task<bool> waitTask = _dryRunActivity.WaitForPurgeRequestAsync(CancellationToken.None).AsTask();
|
|
await Task.Yield();
|
|
|
|
waitTask.IsCompleted.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_WhenRunStartedDryAndHandlerThrows_ExitsActivity()
|
|
{
|
|
GeneralConfig config = await _dataContext.GeneralConfigs.SingleAsync();
|
|
config.DryRun = true;
|
|
await _dataContext.SaveChangesAsync();
|
|
_handler.Throw = new InvalidOperationException("handler exploded");
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
_dryRunActivity.IsActive.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_WhenRunStartedLive_NeverEntersActivity()
|
|
{
|
|
GeneralConfig config = await _dataContext.GeneralConfigs.SingleAsync();
|
|
config.DryRun = false;
|
|
await _dataContext.SaveChangesAsync();
|
|
bool activeDuringExecution = false;
|
|
_handler.OnExecute = () => activeDuringExecution = _dryRunActivity.IsActive;
|
|
|
|
await BuildJob().Execute(Substitute.For<IJobExecutionContext>());
|
|
|
|
activeDuringExecution.ShouldBeFalse();
|
|
_dryRunActivity.IsActive.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Named after a <see cref="JobType"/> member because the job parses its type name.
|
|
/// </summary>
|
|
public sealed class Seeker : IHandler
|
|
{
|
|
public Exception? Throw { get; set; }
|
|
|
|
public bool ObservedDryRun { get; private set; }
|
|
|
|
public Action? OnExecute { get; set; }
|
|
|
|
public Task ExecuteAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
ObservedDryRun = ContextProvider.IsDryRunSticky();
|
|
OnExecute?.Invoke();
|
|
return Throw is null ? Task.CompletedTask : Task.FromException(Throw);
|
|
}
|
|
}
|
|
}
|