Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
@@ -5,59 +5,60 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services;
public sealed class TrackStreamIssuerTests
namespace Musify.Application.Tests.Services
{
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
public TrackStreamIssuerTests() =>
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("signed-token", 120));
private TrackStreamIssuer CreateIssuer(PlaybackConfiguration? playback = null) => new(
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test/"),
playback ?? TestConfigurations.Playback());
[Fact]
public void Issue_BuildsManifestUrlAndCarriesTheListenId()
public sealed class TrackStreamIssuerTests
{
var listenId = Guid.NewGuid();
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
var response = CreateIssuer().Issue("folder-42", userId: 7, listenId);
public TrackStreamIssuerTests() =>
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("signed-token", 120));
Assert.Equal("signed-token", response.Ticket);
Assert.Equal(120, response.ExpiresInSeconds);
Assert.Equal(listenId, response.ListenId);
Assert.Equal("https://stream.musify.test/media/Tracks/ProcessedAudios/folder-42/audio.m4a", response.ManifestUrl);
}
private TrackStreamIssuer CreateIssuer(PlaybackConfiguration? playback = null) => new(
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test/"),
playback ?? TestConfigurations.Playback());
[Fact]
public void Issue_PassesTheProcessedAudioKeyPrefixToTheTicketService()
{
CreateIssuer().Issue("folder-42", userId: 1, listenId: null);
[Fact]
public void Issue_BuildsManifestUrlAndCarriesTheListenId()
{
var listenId = Guid.NewGuid();
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
var response = CreateIssuer().Issue("folder-42", userId: 7, listenId);
[Fact]
public void Issue_AnonymousUserWithFragmentConfigured_PassesTheByteCapToTheTicketService()
{
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
Assert.Equal("signed-token", response.Ticket);
Assert.Equal(120, response.ExpiresInSeconds);
Assert.Equal(listenId, response.ListenId);
Assert.Equal("https://stream.musify.test/media/Tracks/ProcessedAudios/folder-42/audio.m4a", response.ManifestUrl);
}
CreateIssuer(playback).Issue("folder-42", userId: null, listenId: null);
[Fact]
public void Issue_PassesTheProcessedAudioKeyPrefixToTheTicketService()
{
CreateIssuer().Issue("folder-42", userId: 1, listenId: null);
ticketService.Received(1).IssueTicket(null, "Tracks/ProcessedAudios/folder-42/", 30L * playback.EstimatedAudioBytesPerSecond);
}
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
[Fact]
public void Issue_AuthenticatedUser_IgnoresTheAnonymousFragmentCap()
{
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
[Fact]
public void Issue_AnonymousUserWithFragmentConfigured_PassesTheByteCapToTheTicketService()
{
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
CreateIssuer(playback).Issue("folder-42", userId: 1, listenId: null);
CreateIssuer(playback).Issue("folder-42", userId: null, listenId: null);
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
ticketService.Received(1).IssueTicket(null, "Tracks/ProcessedAudios/folder-42/", 30L * playback.EstimatedAudioBytesPerSecond);
}
[Fact]
public void Issue_AuthenticatedUser_IgnoresTheAnonymousFragmentCap()
{
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
CreateIssuer(playback).Issue("folder-42", userId: 1, listenId: null);
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
}
}
@@ -7,152 +7,153 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services;
public sealed class UploadIntentValidatorTests : HandlerTestBase
namespace Musify.Application.Tests.Services
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private readonly Domain.Entities.User owner = TestEntities.User();
private UploadIntentValidator CreateValidator(UploadIntentConfiguration? config = null) =>
new(Database, storageService, config ?? TestConfigurations.UploadIntent());
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectWithinLimits_ReturnsTheIntent()
public sealed class UploadIntentValidatorTests : HandlerTestBase
{
storageService
.HeadObjectAsync("bucket", "temp/object.webp", Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private readonly Domain.Entities.User owner = TestEntities.User();
var intent = TestEntities.UploadIntent(owner.Id, bucket: "bucket", key: "temp/object.webp");
await SeedAsync(owner, intent);
private UploadIntentValidator CreateValidator(UploadIntentConfiguration? config = null) =>
new(Database, storageService, config ?? TestConfigurations.UploadIntent());
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectWithinLimits_ReturnsTheIntent()
{
storageService
.HeadObjectAsync("bucket", "temp/object.webp", Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
Assert.False(result.IsError);
Assert.Equal(intent.Id, result.Value.Id);
}
var intent = TestEntities.UploadIntent(owner.Id, bucket: "bucket", key: "temp/object.webp");
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_IntentIssuedForAnotherPurpose_ReturnsValidationError()
{
var intent = TestEntities.UploadIntent(owner.Id, UploadIntentPurpose.TrackAudio);
await SeedAsync(owner, intent);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(intent.Id, result.Value.Id);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task ValidateAndLoadAsync_IntentIssuedForAnotherPurpose_ReturnsValidationError()
{
var intent = TestEntities.UploadIntent(owner.Id, UploadIntentPurpose.TrackAudio);
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_IntentMissing_ReturnsNotFound()
{
var result = await CreateValidator().ValidateAndLoadAsync(Guid.NewGuid(), owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task ValidateAndLoadAsync_IntentBelongsToAnotherUser_ReturnsNotFound()
{
var stranger = TestEntities.User(2, "stranger");
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, stranger, intent);
[Fact]
public async Task ValidateAndLoadAsync_IntentMissing_ReturnsNotFound()
{
var result = await CreateValidator().ValidateAndLoadAsync(Guid.NewGuid(), owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, stranger.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task ValidateAndLoadAsync_IntentBelongsToAnotherUser_ReturnsNotFound()
{
var stranger = TestEntities.User(2, "stranger");
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, stranger, intent);
[Fact]
public async Task ValidateAndLoadAsync_AlreadyConsumed_ReturnsConflict()
{
var intent = TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed);
await SeedAsync(owner, intent);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, stranger.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
[Fact]
public async Task ValidateAndLoadAsync_AlreadyConsumed_ReturnsConflict()
{
var intent = TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed);
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_PastExpiry_ReturnsValidationError()
{
var intent = TestEntities.UploadIntent(owner.Id, expiresAt: DateTime.UtcNow.AddMinutes(-1));
await SeedAsync(owner, intent);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task ValidateAndLoadAsync_PastExpiry_ReturnsValidationError()
{
var intent = TestEntities.UploadIntent(owner.Id, expiresAt: DateTime.UtcNow.AddMinutes(-1));
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_ObjectNeverUploaded_ReturnsNotFound()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns((ObjectMetaData?)null);
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, intent);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_ObjectNeverUploaded_ReturnsNotFound()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns((ObjectMetaData?)null);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectExceedsMaxSize_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxUploadBytes = 1024;
var result = await CreateValidator().ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectExceedsMaxSize_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxUploadBytes = 1024;
var result = await CreateValidator(config).ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, intent);
[Fact]
public async Task CheckQuotaAsync_WithinLimits_ReturnsSuccess()
{
await SeedAsync(owner);
var result = await CreateValidator(config).ValidateAndLoadAsync(intent.Id, owner.Id, UploadIntentPurpose.TrackPicture, TestContext.Current.CancellationToken);
var result = await CreateValidator().CheckQuotaAsync(owner.Id, requiredBytes: 1024, requiredIntentCount: 1, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
Assert.False(result.IsError);
}
[Fact]
public async Task CheckQuotaAsync_WithinLimits_ReturnsSuccess()
{
await SeedAsync(owner);
[Fact]
public async Task CheckQuotaAsync_TooManyActiveIntents_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
await SeedAsync(owner, TestEntities.UploadIntent(owner.Id));
var result = await CreateValidator().CheckQuotaAsync(owner.Id, requiredBytes: 1024, requiredIntentCount: 1, TestContext.Current.CancellationToken);
var result = await CreateValidator(config).CheckQuotaAsync(owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task CheckQuotaAsync_TooManyActiveIntents_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
await SeedAsync(owner, TestEntities.UploadIntent(owner.Id));
[Fact]
public async Task CheckQuotaAsync_ExpiredOrConsumedIntentsDoNotCountTowardsTheLimit()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
await SeedAsync(
owner,
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Expired),
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed));
var result = await CreateValidator(config).CheckQuotaAsync(owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
var result = await CreateValidator(config).CheckQuotaAsync(owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
Assert.False(result.IsError);
[Fact]
public async Task CheckQuotaAsync_ExpiredOrConsumedIntentsDoNotCountTowardsTheLimit()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
await SeedAsync(
owner,
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Expired),
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed));
var result = await CreateValidator(config).CheckQuotaAsync(owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
}
}
}