Add editorconfig and analyzers, format backend code

This commit is contained in:
2026-09-24 04:54:17 +02:00
parent 87c3f97529
commit c8fe28b07b
367 changed files with 12179 additions and 12459 deletions
@@ -5,108 +5,107 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
{
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
private AddTrackToAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToAlbumCommandHandler>());
[Fact]
public async Task Handle_FirstTrack_AddsItAtNumberOne()
{
private AddTrackToAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToAlbumCommandHandler>());
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, album, track);
[Fact]
public async Task Handle_FirstTrack_AddsItAtNumberOne()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, album, track);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var link = Assert.Single(await Database.AlbumHasTracks.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(1, link.TrackNumber);
}
Assert.False(result.IsError);
var link = Assert.Single(await Database.AlbumHasTracks.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(1, link.TrackNumber);
}
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, Guid.NewGuid(), track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, Guid.NewGuid(), track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, album, track);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, album, track);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(stranger.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(stranger.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackOwnedByAnotherUser_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var otherOwner = TestEntities.User(2, "other");
var album = TestEntities.Album(owner.Id);
var foreignTrack = TestEntities.Track(otherOwner);
await SeedAsync(owner, otherOwner, album, foreignTrack);
[Fact]
public async Task Handle_TrackOwnedByAnotherUser_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var otherOwner = TestEntities.User(2, "other");
var album = TestEntities.Album(owner.Id);
var foreignTrack = TestEntities.Track(otherOwner);
await SeedAsync(owner, otherOwner, album, foreignTrack);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, foreignTrack.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, foreignTrack.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
[Fact]
public async Task Handle_AlreadyInAlbum_ReturnsConflict()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
await SeedAsync(owner, album, track, link);
[Fact]
public async Task Handle_AlreadyInAlbum_ReturnsConflict()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
await SeedAsync(owner, album, track, link);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
[Fact]
public async Task Handle_AlbumAlreadyHasTracks_AppendsAtNextNumber()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var firstTrack = TestEntities.Track(owner, "First");
var secondTrack = TestEntities.Track(owner, "Second");
var existingLink = new AlbumHasTrack { AlbumId = album.Id, TrackId = firstTrack.Id, TrackNumber = 3 };
await SeedAsync(owner, album, firstTrack, secondTrack, existingLink);
[Fact]
public async Task Handle_AlbumAlreadyHasTracks_AppendsAtNextNumber()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var firstTrack = TestEntities.Track(owner, "First");
var secondTrack = TestEntities.Track(owner, "Second");
var existingLink = new AlbumHasTrack { AlbumId = album.Id, TrackId = firstTrack.Id, TrackNumber = 3 };
await SeedAsync(owner, album, firstTrack, secondTrack, existingLink);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, secondTrack.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, secondTrack.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var newLink = await Database.AlbumHasTracks.SingleAsync(l => l.TrackId == secondTrack.Id, TestContext.Current.CancellationToken);
Assert.Equal(4, newLink.TrackNumber);
}
Assert.False(result.IsError);
var newLink = await Database.AlbumHasTracks.SingleAsync(l => l.TrackId == secondTrack.Id, TestContext.Current.CancellationToken);
Assert.Equal(4, newLink.TrackNumber);
}
}
@@ -6,72 +6,71 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
{
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private CreateAlbumCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<CreateAlbumCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Album(),
TestConfigurations.UploadIntent());
[Fact]
public async Task Handle_ValidPictureIntent_CreatesAlbumAndPublishesResourcesEvent()
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
private CreateAlbumCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<CreateAlbumCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Album(),
TestConfigurations.UploadIntent());
var owner = TestEntities.User();
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "cover.webp");
await SeedAsync(owner, intent);
[Fact]
public async Task Handle_ValidPictureIntent_CreatesAlbumAndPublishesResourcesEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
var command = new CreateAlbumCommand(owner.Id, " My Album ", "A description", 2024, intent.Id);
var owner = TestEntities.User();
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "cover.webp");
await SeedAsync(owner, intent);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreateAlbumCommand(owner.Id, " My Album ", "A description", 2024, intent.Id);
Assert.False(result.IsError);
Assert.Equal("My Album", result.Value.Title);
Assert.Equal("A description", result.Value.Description);
Assert.Equal(2024, result.Value.ReleaseYear);
Assert.Equal(owner.Id, result.Value.OwnerUserId);
Assert.Equal(0, result.Value.TrackCount);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var stored = await Database.Albums.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("MY ALBUM", stored.NormalizedTitle);
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateAlbumResourcesEvent>(), Arg.Any<CancellationToken>());
}
Assert.False(result.IsError);
Assert.Equal("My Album", result.Value.Title);
Assert.Equal("A description", result.Value.Description);
Assert.Equal(2024, result.Value.ReleaseYear);
Assert.Equal(owner.Id, result.Value.OwnerUserId);
Assert.Equal(0, result.Value.TrackCount);
[Fact]
public async Task Handle_UserDoesNotExist_ReturnsNotFound()
{
var command = new CreateAlbumCommand(404, "Orphan Album", null, null, Guid.NewGuid());
var stored = await Database.Albums.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("MY ALBUM", stored.NormalizedTitle);
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateAlbumResourcesEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserDoesNotExist_ReturnsNotFound()
{
var command = new CreateAlbumCommand(404, "Orphan Album", null, null, Guid.NewGuid());
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PictureIntentNotFound_ReturnsError()
{
var owner = TestEntities.User();
await SeedAsync(owner);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var command = new CreateAlbumCommand(owner.Id, "My Album", null, null, Guid.NewGuid());
[Fact]
public async Task Handle_PictureIntentNotFound_ReturnsError()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreateAlbumCommand(owner.Id, "My Album", null, null, Guid.NewGuid());
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.True(result.IsError);
}
Assert.True(result.IsError);
}
}
@@ -3,45 +3,44 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
{
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
private DeleteAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<DeleteAlbumCommandHandler>());
[Fact]
public async Task Handle_Owner_RemovesAlbum()
{
private DeleteAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<DeleteAlbumCommandHandler>());
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
[Fact]
public async Task Handle_Owner_RemovesAlbum()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
var result = await CreateHandler().Handle(new DeleteAlbumCommand(owner.Id, album.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new DeleteAlbumCommand(owner.Id, album.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Null(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Null(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeleteAlbumCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeleteAlbumCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorizedAndKeepsAlbum()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, stranger, album);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorizedAndKeepsAlbum()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, stranger, album);
var result = await CreateHandler().Handle(new DeleteAlbumCommand(stranger.Id, album.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new DeleteAlbumCommand(stranger.Id, album.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.NotNull(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.NotNull(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
}
}
@@ -4,34 +4,33 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
private GetAlbumByIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ExistingAlbum_ReturnsItWithTrackCountAndCovers()
{
private GetAlbumByIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, album, track, new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 });
[Fact]
public async Task Handle_ExistingAlbum_ReturnsItWithTrackCountAndCovers()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, album, track, new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 });
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(album.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(album.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(album.Id, result.Value.Id);
Assert.Equal(1, result.Value.TrackCount);
Assert.Equal([track.Id], result.Value.CoverTrackIds);
}
Assert.False(result.IsError);
Assert.Equal(album.Id, result.Value.Id);
Assert.Equal(1, result.Value.TrackCount);
Assert.Equal([track.Id], result.Value.CoverTrackIds);
}
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -3,36 +3,35 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumCoverQueryHandlerTests : HandlerTestBase
{
public sealed class GetAlbumCoverQueryHandlerTests : HandlerTestBase
private GetAlbumCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Album());
[Theory]
[InlineData("small", "small.webp")]
[InlineData("large", "large.webp")]
[InlineData("medium", "medium.webp")]
public async Task Handle_ExistingAlbum_ReturnsRequestedSize(string size, string expectedFileName)
{
private GetAlbumCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Album());
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
[Theory]
[InlineData("small", "small.webp")]
[InlineData("large", "large.webp")]
[InlineData("medium", "medium.webp")]
public async Task Handle_ExistingAlbum_ReturnsRequestedSize(string size, string expectedFileName)
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(album.Id, size), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(album.Id, size), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("covers-bucket", result.Value.Bucket);
Assert.EndsWith(expectedFileName, result.Value.Key);
}
Assert.False(result.IsError);
Assert.Equal("covers-bucket", result.Value.Bucket);
Assert.EndsWith(expectedFileName, result.Value.Key);
}
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -4,36 +4,35 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
{
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
private GetAlbumTracksQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_AlbumWithTracks_ReturnsThemOrderedByTrackNumber()
{
private GetAlbumTracksQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var second = TestEntities.Track(owner, "Second");
var first = TestEntities.Track(owner, "First");
await SeedAsync(
owner, album, second, first,
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 });
[Fact]
public async Task Handle_AlbumWithTracks_ReturnsThemOrderedByTrackNumber()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var second = TestEntities.Track(owner, "Second");
var first = TestEntities.Track(owner, "First");
await SeedAsync(
owner, album, second, first,
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 });
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(album.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(album.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
}
Assert.False(result.IsError);
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
}
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -3,32 +3,31 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
private GetAlbumsByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_UserWithAlbums_ReturnsOnlyTheirAlbums()
{
private GetAlbumsByUserIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
await SeedAsync(owner, other, TestEntities.Album(owner.Id, "Mine"), TestEntities.Album(other.Id, "Theirs"));
[Fact]
public async Task Handle_UserWithAlbums_ReturnsOnlyTheirAlbums()
{
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
await SeedAsync(owner, other, TestEntities.Album(owner.Id, "Mine"), TestEntities.Album(other.Id, "Theirs"));
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(owner.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(owner.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var album = Assert.Single(result.Value.Items);
Assert.Equal("Mine", album.Title);
}
Assert.False(result.IsError);
var album = Assert.Single(result.Value.Items);
Assert.Equal("Mine", album.Title);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(404, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(404, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -2,49 +2,48 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
{
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
private GetAlbumsQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_NoTitleFilter_ReturnsAllAlbumsPaged()
{
private GetAlbumsQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.Album(owner.Id, "Alpha"), TestEntities.Album(owner.Id, "Beta"));
[Fact]
public async Task Handle_NoTitleFilter_ReturnsAllAlbumsPaged()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.Album(owner.Id, "Alpha"), TestEntities.Album(owner.Id, "Beta"));
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.All(result.Value.Items, item => Assert.NotNull(item.Album));
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.All(result.Value.Items, item => Assert.NotNull(item.Album));
}
[Fact]
public async Task Handle_TitleFilter_ReturnsMatchingAlbumsOnly()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.Album(owner.Id, "Rock Classics"), TestEntities.Album(owner.Id, "Jazz Favorites"));
[Fact]
public async Task Handle_TitleFilter_ReturnsMatchingAlbumsOnly()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.Album(owner.Id, "Rock Classics"), TestEntities.Album(owner.Id, "Jazz Favorites"));
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: "rock", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: "rock", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal("Rock Classics", item.Album.Title);
}
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal("Rock Classics", item.Album.Title);
}
[Fact]
public async Task Handle_PageSizeSmallerThanTotal_ReportsHasNextPage()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.Album(owner.Id, "One"), TestEntities.Album(owner.Id, "Two"), TestEntities.Album(owner.Id, "Three"));
[Fact]
public async Task Handle_PageSizeSmallerThanTotal_ReportsHasNextPage()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.Album(owner.Id, "One"), TestEntities.Album(owner.Id, "Two"), TestEntities.Album(owner.Id, "Three"));
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 2), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 2), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.Items.Count);
Assert.True(result.Value.HasNextPage);
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.Items.Count);
Assert.True(result.Value.HasNextPage);
}
}
@@ -3,79 +3,78 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
{
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
private GetRecentlyListenedAlbumsQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ListenedTracks_ReturnsTheirAlbumsMostRecentFirst()
{
private GetRecentlyListenedAlbumsQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var olderAlbum = TestEntities.Album(owner.Id, "Older");
var newerAlbum = TestEntities.Album(owner.Id, "Newer");
var olderTrack = TestEntities.Track(owner, "In older album");
var newerTrack = TestEntities.Track(owner, "In newer album");
await SeedAsync(
owner, olderAlbum, newerAlbum, olderTrack, newerTrack,
new AlbumHasTrack { AlbumId = olderAlbum.Id, TrackId = olderTrack.Id, TrackNumber = 1 },
new AlbumHasTrack { AlbumId = newerAlbum.Id, TrackId = newerTrack.Id, TrackNumber = 1 },
TestEntities.ListeningHistory(owner.Id, olderTrack.Id, DateTime.UtcNow.AddDays(-2)),
TestEntities.ListeningHistory(owner.Id, newerTrack.Id, DateTime.UtcNow.AddDays(-1)));
[Fact]
public async Task Handle_ListenedTracks_ReturnsTheirAlbumsMostRecentFirst()
{
var owner = TestEntities.User();
var olderAlbum = TestEntities.Album(owner.Id, "Older");
var newerAlbum = TestEntities.Album(owner.Id, "Newer");
var olderTrack = TestEntities.Track(owner, "In older album");
var newerTrack = TestEntities.Track(owner, "In newer album");
await SeedAsync(
owner, olderAlbum, newerAlbum, olderTrack, newerTrack,
new AlbumHasTrack { AlbumId = olderAlbum.Id, TrackId = olderTrack.Id, TrackNumber = 1 },
new AlbumHasTrack { AlbumId = newerAlbum.Id, TrackId = newerTrack.Id, TrackNumber = 1 },
TestEntities.ListeningHistory(owner.Id, olderTrack.Id, DateTime.UtcNow.AddDays(-2)),
TestEntities.ListeningHistory(owner.Id, newerTrack.Id, DateTime.UtcNow.AddDays(-1)));
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(["Newer", "Older"], result.Value.Select(album => album.Title));
}
Assert.False(result.IsError);
Assert.Equal(["Newer", "Older"], result.Value.Select(album => album.Title));
}
[Fact]
public async Task Handle_NoListeningHistory_ReturnsEmptyList()
{
var owner = TestEntities.User();
await SeedAsync(owner);
[Fact]
public async Task Handle_NoListeningHistory_ReturnsEmptyList()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(result.Value);
}
Assert.False(result.IsError);
Assert.Empty(result.Value);
}
[Fact]
public async Task Handle_LimitLowerThanCandidates_ReturnsOnlyThatManyAlbums()
{
var owner = TestEntities.User();
var albums = Enumerable.Range(1, 3).Select(i => TestEntities.Album(owner.Id, $"Album {i}")).ToList();
var tracks = albums.Select((_, i) => TestEntities.Track(owner, $"Track {i}")).ToList();
var links = albums.Zip(tracks, (album, track) => new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 }).ToList();
var histories = tracks.Select((track, i) => TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddMinutes(-i))).ToList();
await SeedAsync([owner, .. albums, .. tracks, .. links, .. histories]);
[Fact]
public async Task Handle_LimitLowerThanCandidates_ReturnsOnlyThatManyAlbums()
{
var owner = TestEntities.User();
var albums = Enumerable.Range(1, 3).Select(i => TestEntities.Album(owner.Id, $"Album {i}")).ToList();
var tracks = albums.Select((_, i) => TestEntities.Track(owner, $"Track {i}")).ToList();
var links = albums.Zip(tracks, (album, track) => new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 }).ToList();
var histories = tracks.Select((track, i) => TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddMinutes(-i))).ToList();
await SeedAsync([owner, .. albums, .. tracks, .. links, .. histories]);
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 2), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 2), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.Count);
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.Count);
}
[Fact]
public async Task Handle_UncountedListens_AreIgnored()
{
var owner = TestEntities.User();
var countedAlbum = TestEntities.Album(owner.Id, "Counted album");
var skippedAlbum = TestEntities.Album(owner.Id, "Skipped album");
var countedTrack = TestEntities.Track(owner, "In counted");
var skippedTrack = TestEntities.Track(owner, "In skipped");
await SeedAsync(
owner, countedAlbum, skippedAlbum, countedTrack, skippedTrack,
new AlbumHasTrack { AlbumId = countedAlbum.Id, TrackId = countedTrack.Id, TrackNumber = 1 },
new AlbumHasTrack { AlbumId = skippedAlbum.Id, TrackId = skippedTrack.Id, TrackNumber = 1 },
TestEntities.ListeningHistory(owner.Id, countedTrack.Id, DateTime.UtcNow.AddDays(-1), playedSeconds: 120),
TestEntities.ListeningHistory(owner.Id, skippedTrack.Id, DateTime.UtcNow, playedSeconds: 1, isCounted: false));
[Fact]
public async Task Handle_UncountedListens_AreIgnored()
{
var owner = TestEntities.User();
var countedAlbum = TestEntities.Album(owner.Id, "Counted album");
var skippedAlbum = TestEntities.Album(owner.Id, "Skipped album");
var countedTrack = TestEntities.Track(owner, "In counted");
var skippedTrack = TestEntities.Track(owner, "In skipped");
await SeedAsync(
owner, countedAlbum, skippedAlbum, countedTrack, skippedTrack,
new AlbumHasTrack { AlbumId = countedAlbum.Id, TrackId = countedTrack.Id, TrackNumber = 1 },
new AlbumHasTrack { AlbumId = skippedAlbum.Id, TrackId = skippedTrack.Id, TrackNumber = 1 },
TestEntities.ListeningHistory(owner.Id, countedTrack.Id, DateTime.UtcNow.AddDays(-1), playedSeconds: 120),
TestEntities.ListeningHistory(owner.Id, skippedTrack.Id, DateTime.UtcNow, playedSeconds: 1, isCounted: false));
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
Assert.Equal(["Counted album"], result.Value.Select(album => album.Title));
}
Assert.Equal(["Counted album"], result.Value.Select(album => album.Title));
}
}
@@ -5,69 +5,68 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
{
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
private RemoveTrackFromAlbumCommandHandler CreateHandler() =>
new(Database, NoOpLogger<RemoveTrackFromAlbumCommandHandler>());
[Fact]
public async Task Handle_TrackInAlbum_RemovesItAndRenumbersTheRest()
{
private RemoveTrackFromAlbumCommandHandler CreateHandler() =>
new(Database, NoOpLogger<RemoveTrackFromAlbumCommandHandler>());
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var first = TestEntities.Track(owner, "First");
var second = TestEntities.Track(owner, "Second");
var third = TestEntities.Track(owner, "Third");
await SeedAsync(
owner, album, first, second, third,
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 },
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
new AlbumHasTrack { AlbumId = album.Id, TrackId = third.Id, TrackNumber = 3 });
[Fact]
public async Task Handle_TrackInAlbum_RemovesItAndRenumbersTheRest()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
var first = TestEntities.Track(owner, "First");
var second = TestEntities.Track(owner, "Second");
var third = TestEntities.Track(owner, "Third");
await SeedAsync(
owner, album, first, second, third,
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 },
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
new AlbumHasTrack { AlbumId = album.Id, TrackId = third.Id, TrackNumber = 3 });
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, second.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, second.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var remaining = await Database.AlbumHasTracks
.Where(link => link.AlbumId == album.Id)
.OrderBy(link => link.TrackNumber)
.ToListAsync(TestContext.Current.CancellationToken);
Assert.Equal([first.Id, third.Id], remaining.Select(link => link.TrackId));
Assert.Equal([1, 2], remaining.Select(link => link.TrackNumber));
}
Assert.False(result.IsError);
var remaining = await Database.AlbumHasTracks
.Where(link => link.AlbumId == album.Id)
.OrderBy(link => link.TrackNumber)
.ToListAsync(TestContext.Current.CancellationToken);
Assert.Equal([first.Id, third.Id], remaining.Select(link => link.TrackId));
Assert.Equal([1, 2], remaining.Select(link => link.TrackNumber));
}
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, stranger, album);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, stranger, album);
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(stranger.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(stranger.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackNotInAlbum_ReturnsNotFound()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
[Fact]
public async Task Handle_TrackNotInAlbum_ReturnsNotFound()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, album);
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -8,68 +8,67 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class RequestAlbumPictureUploadCommandHandlerTests : HandlerTestBase
{
public sealed class RequestAlbumPictureUploadCommandHandlerTests : HandlerTestBase
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private RequestAlbumPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
storageService
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
.Returns("https://storage.musify.test/presigned-upload");
private RequestAlbumPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
{
storageService
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
.Returns("https://storage.musify.test/presigned-upload");
return new RequestAlbumPictureUploadCommandHandler(
Database,
new UploadIntentValidator(Database, storageService),
storageService,
NoOpLogger<RequestAlbumPictureUploadCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Album(),
uploadIntentConfig ?? TestConfigurations.UploadIntent());
}
return new RequestAlbumPictureUploadCommandHandler(
Database,
new UploadIntentValidator(Database, storageService),
storageService,
NoOpLogger<RequestAlbumPictureUploadCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Album(),
uploadIntentConfig ?? TestConfigurations.UploadIntent());
}
[Fact]
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
{
var user = TestEntities.User();
await SeedAsync(user);
[Fact]
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
{
var user = TestEntities.User();
await SeedAsync(user);
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp");
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp");
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
Assert.Single(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
Assert.Single(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new RequestAlbumPictureUploadCommand(404, "webp", "image/webp");
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new RequestAlbumPictureUploadCommand(404, "webp", "image/webp");
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadBytesPerUser = 100;
[Fact]
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadBytesPerUser = 100;
var user = TestEntities.User();
await SeedAsync(user);
var user = TestEntities.User();
await SeedAsync(user);
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
}
}
@@ -6,89 +6,88 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Albums
namespace Musify.Application.Tests.Albums;
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
{
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private UpdateAlbumCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<UpdateAlbumCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Album(),
TestConfigurations.UploadIntent());
[Fact]
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id, title: "Old Title", description: "Old", releaseYear: 2020);
await SeedAsync(owner, album);
private UpdateAlbumCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<UpdateAlbumCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Album(),
TestConfigurations.UploadIntent());
var command = new UpdateAlbumCommand(owner.Id, album.Id, " New Title ", "New description", 2025, null);
[Fact]
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id, title: "Old Title", description: "Old", releaseYear: 2020);
await SeedAsync(owner, album);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdateAlbumCommand(owner.Id, album.Id, " New Title ", "New description", 2025, null);
Assert.False(result.IsError);
Assert.Equal("New Title", result.Value.Title);
Assert.Equal("New description", result.Value.Description);
Assert.Equal(2025, result.Value.ReleaseYear);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("NEW TITLE", stored.NormalizedTitle);
}
Assert.False(result.IsError);
Assert.Equal("New Title", result.Value.Title);
Assert.Equal("New description", result.Value.Description);
Assert.Equal(2025, result.Value.ReleaseYear);
[Fact]
public async Task Handle_ValidPictureIntent_UpdatesPictureAndPublishesSourceEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("NEW TITLE", stored.NormalizedTitle);
}
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id, title: "Old Title");
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
await SeedAsync(owner, album, intent);
[Fact]
public async Task Handle_ValidPictureIntent_UpdatesPictureAndPublishesSourceEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
var command = new UpdateAlbumCommand(owner.Id, album.Id, "Old Title", null, null, intent.Id);
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id, title: "Old Title");
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
await SeedAsync(owner, album, intent);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdateAlbumCommand(owner.Id, album.Id, "Old Title", null, null, intent.Id);
Assert.False(result.IsError);
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null, null);
Assert.False(result.IsError);
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AlbumMissing_ReturnsNotFound()
{
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null, null);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, stranger, album);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null, null);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var album = TestEntities.Album(owner.Id);
await SeedAsync(owner, stranger, album);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null, null);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
}