Added tests

This commit is contained in:
2026-05-29 13:49:44 +02:00
parent 10124f0184
commit af2cf03fb4
29 changed files with 2212 additions and 6 deletions
@@ -0,0 +1,32 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Musify.Application.Contracts;
using Musify.Application.Tests.Infrastructure;
namespace Musify.Infrastructure.Tests;
internal static class JobTestHarness
{
public static IServiceScopeFactory ScopeFactoryWith(Action<IServiceCollection> configure)
{
var services = new ServiceCollection();
configure(services);
return services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
}
public static IServiceScopeFactory WithDatabase(IDatabase database) =>
ScopeFactoryWith(s => s.AddSingleton(database));
public static IServiceScopeFactory WithStorage(IStorageService storage) =>
ScopeFactoryWith(s => s.AddSingleton(storage));
public static Task RunAsync(object job, CancellationToken cancellationToken = default)
{
var method = job.GetType().GetMethod(
"RunAsync",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
?? throw new InvalidOperationException("RunAsync not found on job.");
return (Task)method.Invoke(job, [cancellationToken])!;
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Musify.Infrastructure.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.csproj" />
<ProjectReference Include="..\Musify.Application.Tests\Musify.Application.Tests.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,72 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Musify.Application.Contracts;
using Musify.Application.Tests.Infrastructure;
using Musify.Infrastructure.Jobs;
using NSubstitute;
using Xunit;
namespace Musify.Infrastructure.Tests;
public sealed class TemporalUploadsCleanUpJobTests
{
private readonly IStorageService _storage = Substitute.For<IStorageService>();
private readonly Application.Configuration.UploadIntentConfiguration _config = TestData.UploadIntentConfig();
private TemporalUploadsCleanUpJob CreateJob() =>
new(
JobTestHarness.WithStorage(_storage),
NullLogger<TemporalUploadsCleanUpJob>.Instance,
TestData.StorageConfig(),
_config);
private static async IAsyncEnumerable<StorageObject> AsAsync(params StorageObject[] items)
{
foreach (var item in items)
yield return item;
await Task.CompletedTask;
}
[Fact]
public async Task Deletes_only_objects_older_than_retention_window()
{
var cutoffDays = _config.TempUploadsRetentionDays;
var stale = new StorageObject("temp/u1/old.webp", DateTime.UtcNow.AddDays(-(cutoffDays + 2)));
var fresh = new StorageObject("temp/u1/new.webp", DateTime.UtcNow.AddHours(-1));
_storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any<CancellationToken>())
.Returns(AsAsync(stale, fresh));
await JobTestHarness.RunAsync(CreateJob());
await _storage.Received(1).RemoveFileAsync(TestData.Bucket, stale.Key, Arg.Any<CancellationToken>());
await _storage.DidNotReceive().RemoveFileAsync(TestData.Bucket, fresh.Key, Arg.Any<CancellationToken>());
}
[Fact]
public async Task Continues_when_a_delete_fails()
{
var first = new StorageObject("temp/u1/a.webp", DateTime.UtcNow.AddDays(-10));
var second = new StorageObject("temp/u1/b.webp", DateTime.UtcNow.AddDays(-10));
_storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any<CancellationToken>())
.Returns(AsAsync(first, second));
_storage.RemoveFileAsync(TestData.Bucket, first.Key, Arg.Any<CancellationToken>())
.Returns<Task>(_ => throw new InvalidOperationException("s3 down"));
await JobTestHarness.RunAsync(CreateJob());
await _storage.Received(1).RemoveFileAsync(TestData.Bucket, second.Key, Arg.Any<CancellationToken>());
}
[Fact]
public async Task Does_nothing_when_no_objects_listed()
{
_storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any<CancellationToken>())
.Returns(AsAsync());
await JobTestHarness.RunAsync(CreateJob());
await _storage.DidNotReceive().RemoveFileAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
}
}
@@ -0,0 +1,84 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Musify.Application.Tests.Infrastructure;
using Musify.Domain.ValueObjects;
using Musify.Infrastructure.Jobs;
using Xunit;
namespace Musify.Infrastructure.Tests;
public sealed class UploadIntentExpirationJobTests
{
private static UploadIntentExpirationJob CreateJob(SqliteTestContext ctx, Action<Application.Configuration.UploadIntentConfiguration>? configure = null)
{
var config = TestData.UploadIntentConfig();
configure?.Invoke(config);
return new UploadIntentExpirationJob(
JobTestHarness.WithDatabase(ctx.Database),
NullLogger<UploadIntentExpirationJob>.Instance,
config);
}
[Fact]
public async Task Marks_issued_intents_past_expiry_as_expired()
{
await using var ctx = new SqliteTestContext();
var expired = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-5));
var active = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(30));
ctx.Database.UploadIntents.AddRange(expired, active);
await ctx.Database.SaveChangesAsync(CancellationToken.None);
await JobTestHarness.RunAsync(CreateJob(ctx));
var verify = ctx.NewContext();
(await verify.UploadIntents.SingleAsync(i => i.Id == expired.Id)).Status.Should().Be(UploadIntentStatus.Expired);
(await verify.UploadIntents.SingleAsync(i => i.Id == active.Id)).Status.Should().Be(UploadIntentStatus.Issued);
}
[Fact]
public async Task Deletes_expired_intents_past_retention_window()
{
await using var ctx = new SqliteTestContext();
var config = TestData.UploadIntentConfig();
var old = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired,
expiresAt: DateTime.UtcNow.AddDays(-(config.ExpiredIntentsRetentionDays + 1)));
var recent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired,
expiresAt: DateTime.UtcNow.AddHours(-1));
ctx.Database.UploadIntents.AddRange(old, recent);
await ctx.Database.SaveChangesAsync(CancellationToken.None);
await JobTestHarness.RunAsync(CreateJob(ctx));
var verify = ctx.NewContext();
(await verify.UploadIntents.AnyAsync(i => i.Id == old.Id)).Should().BeFalse();
(await verify.UploadIntents.AnyAsync(i => i.Id == recent.Id)).Should().BeTrue();
}
[Fact]
public async Task With_zero_retention_just_expired_intents_are_marked_then_deleted()
{
await using var ctx = new SqliteTestContext();
var intent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1));
ctx.Database.UploadIntents.Add(intent);
await ctx.Database.SaveChangesAsync(CancellationToken.None);
await JobTestHarness.RunAsync(CreateJob(ctx, c => c.ExpiredIntentsRetentionDays = 0));
(await ctx.NewContext().UploadIntents.AnyAsync(i => i.Id == intent.Id)).Should().BeFalse();
}
[Fact]
public async Task Leaves_consumed_intents_untouched()
{
await using var ctx = new SqliteTestContext();
var consumed = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Consumed, expiresAt: DateTime.UtcNow.AddDays(-10));
ctx.Database.UploadIntents.Add(consumed);
await ctx.Database.SaveChangesAsync(CancellationToken.None);
await JobTestHarness.RunAsync(CreateJob(ctx));
(await ctx.NewContext().UploadIntents.SingleAsync(i => i.Id == consumed.Id)).Status
.Should().Be(UploadIntentStatus.Consumed);
}
}