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);
}
}
@@ -2,30 +2,29 @@ using Musify.Application.Configuration;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Configuration
namespace Musify.Application.Tests.Configuration;
public sealed class GetPlaybackPublicConfigTests
{
public sealed class GetPlaybackPublicConfigTests
[Fact]
public async Task Handle_ReflectsPlaybackConfiguration()
{
[Fact]
public async Task Handle_ReflectsPlaybackConfiguration()
{
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
var handler = new GetPlaybackPublicConfigQueryHandler(playback);
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
var handler = new GetPlaybackPublicConfigQueryHandler(playback);
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
Assert.True(response.AllowAnonymousListening);
Assert.Equal(30, response.AnonymousFragmentSeconds);
}
Assert.True(response.AllowAnonymousListening);
Assert.Equal(30, response.AnonymousFragmentSeconds);
}
[Fact]
public async Task Handle_DefaultsToDisabled()
{
var handler = new GetPlaybackPublicConfigQueryHandler(TestConfigurations.Playback());
[Fact]
public async Task Handle_DefaultsToDisabled()
{
var handler = new GetPlaybackPublicConfigQueryHandler(TestConfigurations.Playback());
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
Assert.False(response.AllowAnonymousListening);
}
Assert.False(response.AllowAnonymousListening);
}
}
@@ -2,31 +2,30 @@ using Musify.Application.Genres;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.Genres
namespace Musify.Application.Tests.Genres;
public sealed class GetAvailableGenresQueryHandlerTests
{
public sealed class GetAvailableGenresQueryHandlerTests
private static async Task<IReadOnlyList<AvailableGenreResponse>> RunAsync() =>
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ReturnsEveryGenre()
{
private static async Task<IReadOnlyList<AvailableGenreResponse>> RunAsync() =>
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
var result = await RunAsync();
[Fact]
public async Task Handle_ReturnsEveryGenre()
{
var result = await RunAsync();
Assert.Equal(Enum.GetValues<Genre>(), result.Select(r => r.Genre));
}
Assert.Equal(Enum.GetValues<Genre>(), result.Select(r => r.Genre));
}
[Fact]
public async Task Handle_ListsIncompatibleGenresSymmetrically()
{
var result = await RunAsync();
[Fact]
public async Task Handle_ListsIncompatibleGenresSymmetrically()
{
var result = await RunAsync();
var metal = result.Single(r => r.Genre == Genre.Metal);
var lofi = result.Single(r => r.Genre == Genre.Lofi);
Assert.Contains(Genre.Lofi, metal.IncompatibleWith);
Assert.Contains(Genre.Metal, lofi.IncompatibleWith);
Assert.Empty(result.Single(r => r.Genre == Genre.Pop).IncompatibleWith);
}
var metal = result.Single(r => r.Genre == Genre.Metal);
var lofi = result.Single(r => r.Genre == Genre.Lofi);
Assert.Contains(Genre.Lofi, metal.IncompatibleWith);
Assert.Contains(Genre.Metal, lofi.IncompatibleWith);
Assert.Empty(result.Single(r => r.Genre == Genre.Pop).IncompatibleWith);
}
}
@@ -4,66 +4,65 @@ using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.Genres
namespace Musify.Application.Tests.Genres;
public sealed class GetGenresQueryHandlerTests : HandlerTestBase
{
public sealed class GetGenresQueryHandlerTests : HandlerTestBase
private GetGenresQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_NoTracks_ReturnsEmpty()
{
private GetGenresQueryHandler CreateHandler() => new(Database);
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoTracks_ReturnsEmpty()
{
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
Assert.Empty(result);
}
Assert.Empty(result);
}
[Fact]
public async Task Handle_ReturnsOnlyGenresInUse_MostPopulatedFirst()
{
var owner = TestEntities.User();
var rockA = TestEntities.Track(owner, "Rock A", tags: [Genre.Rock]);
var rockB = TestEntities.Track(owner, "Rock B", tags: [Genre.Rock, Genre.Indie]);
var jazz = TestEntities.Track(owner, "Jazz", tags: [Genre.Jazz]);
await SeedAsync(
owner, rockA, rockB, jazz,
new UserHasTrack { UserId = owner.Id, TrackId = rockA.Id },
new UserHasTrack { UserId = owner.Id, TrackId = rockB.Id },
new UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
[Fact]
public async Task Handle_ReturnsOnlyGenresInUse_MostPopulatedFirst()
{
var owner = TestEntities.User();
var rockA = TestEntities.Track(owner, "Rock A", tags: [Genre.Rock]);
var rockB = TestEntities.Track(owner, "Rock B", tags: [Genre.Rock, Genre.Indie]);
var jazz = TestEntities.Track(owner, "Jazz", tags: [Genre.Jazz]);
await SeedAsync(
owner, rockA, rockB, jazz,
new UserHasTrack { UserId = owner.Id, TrackId = rockA.Id },
new UserHasTrack { UserId = owner.Id, TrackId = rockB.Id },
new UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
Assert.Equal(
[(Genre.Rock, 2), (Genre.Jazz, 1), (Genre.Indie, 1)],
result.Select(r => (r.Genre, r.TrackCount)));
}
Assert.Equal(
[(Genre.Rock, 2), (Genre.Jazz, 1), (Genre.Indie, 1)],
result.Select(r => (r.Genre, r.TrackCount)));
}
[Fact]
public async Task Handle_TrackInSeveralLibraries_IsCountedOnce()
{
var owner = TestEntities.User();
var other = TestEntities.User(id: 2, name: "other-user");
var track = TestEntities.Track(owner, tags: [Genre.Pop]);
await SeedAsync(
owner, other, track,
new UserHasTrack { UserId = owner.Id, TrackId = track.Id },
new UserHasTrack { UserId = other.Id, TrackId = track.Id });
[Fact]
public async Task Handle_TrackInSeveralLibraries_IsCountedOnce()
{
var owner = TestEntities.User();
var other = TestEntities.User(id: 2, name: "other-user");
var track = TestEntities.Track(owner, tags: [Genre.Pop]);
await SeedAsync(
owner, other, track,
new UserHasTrack { UserId = owner.Id, TrackId = track.Id },
new UserHasTrack { UserId = other.Id, TrackId = track.Id });
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
Assert.Equal(1, Assert.Single(result).TrackCount);
}
Assert.Equal(1, Assert.Single(result).TrackCount);
}
[Fact]
public async Task Handle_TrackNotInAnyLibrary_IsIgnored()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, tags: [Genre.Metal]);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_TrackNotInAnyLibrary_IsIgnored()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, tags: [Genre.Metal]);
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
Assert.Empty(result);
}
Assert.Empty(result);
}
}
@@ -3,53 +3,52 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Likes
namespace Musify.Application.Tests.Likes;
public sealed class GetLikedTracksQueryHandlerTests : HandlerTestBase
{
public sealed class GetLikedTracksQueryHandlerTests : HandlerTestBase
private GetLikedTracksQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_MultipleLikes_ReturnsMostRecentFirst()
{
private GetLikedTracksQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var older = TestEntities.Track(owner, "Older like");
var newer = TestEntities.Track(owner, "Newer like");
await SeedAsync(
owner, older, newer,
new TrackLike { UserId = owner.Id, TrackId = older.Id, CreatedAt = DateTime.UtcNow.AddMinutes(-10) },
new TrackLike { UserId = owner.Id, TrackId = newer.Id, CreatedAt = DateTime.UtcNow });
[Fact]
public async Task Handle_MultipleLikes_ReturnsMostRecentFirst()
{
var owner = TestEntities.User();
var older = TestEntities.Track(owner, "Older like");
var newer = TestEntities.Track(owner, "Newer like");
await SeedAsync(
owner, older, newer,
new TrackLike { UserId = owner.Id, TrackId = older.Id, CreatedAt = DateTime.UtcNow.AddMinutes(-10) },
new TrackLike { UserId = owner.Id, TrackId = newer.Id, CreatedAt = DateTime.UtcNow });
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(["Newer like", "Older like"], result.Value.Items.Select(track => track.Title));
}
Assert.False(result.IsError);
Assert.Equal(["Newer like", "Older like"], result.Value.Items.Select(track => track.Title));
}
[Fact]
public async Task Handle_NoLikes_ReturnsEmpty()
{
var owner = TestEntities.User();
await SeedAsync(owner);
[Fact]
public async Task Handle_NoLikes_ReturnsEmpty()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
[Fact]
public async Task Handle_OtherUsersLikes_AreExcluded()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, track, new TrackLike { UserId = stranger.Id, TrackId = track.Id });
[Fact]
public async Task Handle_OtherUsersLikes_AreExcluded()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, track, new TrackLike { UserId = stranger.Id, TrackId = track.Id });
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
}
@@ -4,53 +4,52 @@ using Musify.Application.Likes;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Likes
namespace Musify.Application.Tests.Likes;
public sealed class ToggleTrackLikeCommandHandlerTests : HandlerTestBase
{
public sealed class ToggleTrackLikeCommandHandlerTests : HandlerTestBase
private ToggleTrackLikeCommandHandler CreateHandler() =>
new(Database, NoOpLogger<ToggleTrackLikeCommandHandler>());
[Fact]
public async Task Handle_TrackNotLiked_LikesIt()
{
private ToggleTrackLikeCommandHandler CreateHandler() =>
new(Database, NoOpLogger<ToggleTrackLikeCommandHandler>());
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_TrackNotLiked_LikesIt()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.True(result.Value);
Assert.Single(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.True(result.Value);
Assert.Single(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_TrackAlreadyLiked_UnlikesIt()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track, new Musify.Domain.Entities.TrackLike { UserId = owner.Id, TrackId = track.Id });
[Fact]
public async Task Handle_TrackAlreadyLiked_UnlikesIt()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track, new Musify.Domain.Entities.TrackLike { UserId = owner.Id, TrackId = track.Id });
var result = await CreateHandler().Handle(
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.False(result.Value);
Assert.Empty(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.False(result.Value);
Assert.Empty(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
await SeedAsync(owner);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(
new ToggleTrackLikeCommand(owner.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(
new ToggleTrackLikeCommand(owner.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -4,89 +4,88 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Mixes
namespace Musify.Application.Tests.Mixes;
public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
{
public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
private GenerateMixesForUserCommandHandler CreateHandler() => new(
Database, TestConfigurations.Mix(), NoOpLogger<GenerateMixesForUserCommandHandler>());
[Fact]
public async Task Handle_NoTracks_DoesNotCreateAnyMix()
{
private GenerateMixesForUserCommandHandler CreateHandler() => new(
Database, TestConfigurations.Mix(), NoOpLogger<GenerateMixesForUserCommandHandler>());
var user = TestEntities.User();
await SeedAsync(user);
[Fact]
public async Task Handle_NoTracks_DoesNotCreateAnyMix()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Empty(await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_AllTracksAlreadyListenedTo_CreatesOnlyTheDailyMix()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
new UserHasTrack { UserId = user.Id, TrackId = track.Id },
TestEntities.ListeningHistory(user.Id, track.Id));
[Fact]
public async Task Handle_AllTracksAlreadyListenedTo_CreatesOnlyTheDailyMix()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
new UserHasTrack { UserId = user.Id, TrackId = track.Id },
TestEntities.ListeningHistory(user.Id, track.Id));
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
Assert.Single(mixes);
Assert.Equal("Tu mezcla diaria", mixes[0].Title);
}
Assert.False(result.IsError);
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
Assert.Single(mixes);
Assert.Equal("Tu mezcla diaria", mixes[0].Title);
}
[Fact]
public async Task Handle_UnheardTracksInLibrary_CreatesDiscoveryAndDailyMixes_AndReplacesPreviousMixes()
{
var user = TestEntities.User();
var listenedTrack = TestEntities.Track(user, "Listened Track");
var unheardTrack = TestEntities.Track(user, "Unheard Track");
[Fact]
public async Task Handle_UnheardTracksInLibrary_CreatesDiscoveryAndDailyMixes_AndReplacesPreviousMixes()
{
var user = TestEntities.User();
var listenedTrack = TestEntities.Track(user, "Listened Track");
var unheardTrack = TestEntities.Track(user, "Unheard Track");
var staleMix = TestEntities.Mix(user.Id, "Stale Mix");
var staleItem = TestEntities.MixItem(staleMix.Id, listenedTrack.Id);
var staleMix = TestEntities.Mix(user.Id, "Stale Mix");
var staleItem = TestEntities.MixItem(staleMix.Id, listenedTrack.Id);
await SeedAsync(
user, listenedTrack, unheardTrack, staleMix, staleItem,
new UserHasTrack { UserId = user.Id, TrackId = listenedTrack.Id },
new UserHasTrack { UserId = user.Id, TrackId = unheardTrack.Id },
TestEntities.ListeningHistory(user.Id, listenedTrack.Id));
await SeedAsync(
user, listenedTrack, unheardTrack, staleMix, staleItem,
new UserHasTrack { UserId = user.Id, TrackId = listenedTrack.Id },
new UserHasTrack { UserId = user.Id, TrackId = unheardTrack.Id },
TestEntities.ListeningHistory(user.Id, listenedTrack.Id));
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
Assert.DoesNotContain(mixes, mix => mix.Title == "Stale Mix");
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
Assert.Contains(mixes, mix => mix.Title == "Tu mezcla diaria");
Assert.Empty(await Database.MixItems.Where(item => item.MixId == staleMix.Id).ToListAsync(TestContext.Current.CancellationToken));
Assert.False(result.IsError);
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
Assert.DoesNotContain(mixes, mix => mix.Title == "Stale Mix");
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
Assert.Contains(mixes, mix => mix.Title == "Tu mezcla diaria");
Assert.Empty(await Database.MixItems.Where(item => item.MixId == staleMix.Id).ToListAsync(TestContext.Current.CancellationToken));
var discoveryMix = mixes.Single(mix => mix.Title == "Descubrimiento");
var discoveryItems = await Database.MixItems
.Where(item => item.MixId == discoveryMix.Id)
.ToListAsync(TestContext.Current.CancellationToken);
Assert.All(discoveryItems, item => Assert.Equal(unheardTrack.Id, item.TrackId));
}
var discoveryMix = mixes.Single(mix => mix.Title == "Descubrimiento");
var discoveryItems = await Database.MixItems
.Where(item => item.MixId == discoveryMix.Id)
.ToListAsync(TestContext.Current.CancellationToken);
Assert.All(discoveryItems, item => Assert.Equal(unheardTrack.Id, item.TrackId));
}
[Fact]
public async Task Handle_TrackWithOnlyUncountedListens_IsTreatedAsUnheard()
{
var user = TestEntities.User();
var skipped = TestEntities.Track(user, "Only skipped");
await SeedAsync(
user, skipped,
new UserHasTrack { UserId = user.Id, TrackId = skipped.Id },
TestEntities.ListeningHistory(user.Id, skipped.Id, playedSeconds: 3, isCounted: false));
[Fact]
public async Task Handle_TrackWithOnlyUncountedListens_IsTreatedAsUnheard()
{
var user = TestEntities.User();
var skipped = TestEntities.Track(user, "Only skipped");
await SeedAsync(
user, skipped,
new UserHasTrack { UserId = user.Id, TrackId = skipped.Id },
TestEntities.ListeningHistory(user.Id, skipped.Id, playedSeconds: 3, isCounted: false));
await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
}
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
}
}
@@ -3,48 +3,47 @@ using Musify.Application.Mixes;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Mixes
namespace Musify.Application.Tests.Mixes;
public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
private GetMixByIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_OwnedMix_ReturnsItWithOrderedItems()
{
private GetMixByIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var firstTrack = TestEntities.Track(owner, "First");
var secondTrack = TestEntities.Track(owner, "Second");
var mix = TestEntities.Mix(owner.Id, "Discovery");
var second = TestEntities.MixItem(mix.Id, secondTrack.Id, position: 1);
var first = TestEntities.MixItem(mix.Id, firstTrack.Id, position: 0);
await SeedAsync(owner, firstTrack, secondTrack, mix, second, first);
[Fact]
public async Task Handle_OwnedMix_ReturnsItWithOrderedItems()
{
var owner = TestEntities.User();
var firstTrack = TestEntities.Track(owner, "First");
var secondTrack = TestEntities.Track(owner, "Second");
var mix = TestEntities.Mix(owner.Id, "Discovery");
var second = TestEntities.MixItem(mix.Id, secondTrack.Id, position: 1);
var first = TestEntities.MixItem(mix.Id, firstTrack.Id, position: 0);
await SeedAsync(owner, firstTrack, secondTrack, mix, second, first);
var result = await CreateHandler().Handle(new GetMixByIdQuery(owner.Id, mix.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetMixByIdQuery(owner.Id, mix.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("Discovery", result.Value.Title);
Assert.Equal(["First", "Second"], result.Value.Items.Select(item => item.Title));
}
Assert.False(result.IsError);
Assert.Equal("Discovery", result.Value.Title);
Assert.Equal(["First", "Second"], result.Value.Items.Select(item => item.Title));
}
[Fact]
public async Task Handle_MixMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetMixByIdQuery(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_MixMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetMixByIdQuery(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_MixBelongsToAnotherUser_ReturnsNotFound()
{
var owner = TestEntities.User(1, "owner");
var mix = TestEntities.Mix(owner.Id);
await SeedAsync(owner, mix);
[Fact]
public async Task Handle_MixBelongsToAnotherUser_ReturnsNotFound()
{
var owner = TestEntities.User(1, "owner");
var mix = TestEntities.Mix(owner.Id);
await SeedAsync(owner, mix);
var result = await CreateHandler().Handle(new GetMixByIdQuery(2, mix.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetMixByIdQuery(2, mix.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -2,38 +2,37 @@ using Musify.Application.Mixes;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Mixes
namespace Musify.Application.Tests.Mixes;
public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
private GetMixesByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ReturnsOnlyThatUsersMixesOrderedByPosition()
{
private GetMixesByUserIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
var second = TestEntities.Mix(owner.Id, "Second", position: 1);
var first = TestEntities.Mix(owner.Id, "First", position: 0);
var theirs = TestEntities.Mix(other.Id, "Theirs");
await SeedAsync(owner, other, second, first, theirs);
[Fact]
public async Task Handle_ReturnsOnlyThatUsersMixesOrderedByPosition()
{
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
var second = TestEntities.Mix(owner.Id, "Second", position: 1);
var first = TestEntities.Mix(owner.Id, "First", position: 0);
var theirs = TestEntities.Mix(other.Id, "Theirs");
await SeedAsync(owner, other, second, first, theirs);
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(["First", "Second"], result.Value.Select(mix => mix.Title));
}
Assert.False(result.IsError);
Assert.Equal(["First", "Second"], result.Value.Select(mix => mix.Title));
}
[Fact]
public async Task Handle_NoMixes_ReturnsEmptyList()
{
var owner = TestEntities.User();
await SeedAsync(owner);
[Fact]
public async Task Handle_NoMixes_ReturnsEmptyList()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(result.Value);
}
Assert.False(result.IsError);
Assert.Empty(result.Value);
}
}
@@ -5,89 +5,88 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
{
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
private AddTrackToPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToPlayListCommandHandler>());
[Fact]
public async Task Handle_FirstTrack_AddsItAtPositionZero()
{
private AddTrackToPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToPlayListCommandHandler>());
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track);
[Fact]
public async Task Handle_FirstTrack_AddsItAtPositionZero()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var link = Assert.Single(await Database.PlayListHasTracks.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(0, link.Position);
}
Assert.False(result.IsError);
var link = Assert.Single(await Database.PlayListHasTracks.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(0, link.Position);
}
[Fact]
public async Task Handle_PlayListAlreadyHasTracks_AppendsAtNextPosition()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var existingTrack = TestEntities.Track(owner, "Existing");
var newTrack = TestEntities.Track(owner, "New");
await SeedAsync(
owner, playList, existingTrack, newTrack,
new PlayListHasTrack { PlayListId = playList.Id, TrackId = existingTrack.Id, Position = 0 });
[Fact]
public async Task Handle_PlayListAlreadyHasTracks_AppendsAtNextPosition()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var existingTrack = TestEntities.Track(owner, "Existing");
var newTrack = TestEntities.Track(owner, "New");
await SeedAsync(
owner, playList, existingTrack, newTrack,
new PlayListHasTrack { PlayListId = playList.Id, TrackId = existingTrack.Id, Position = 0 });
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, newTrack.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, newTrack.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var newLink = await Database.PlayListHasTracks.SingleAsync(link => link.TrackId == newTrack.Id, TestContext.Current.CancellationToken);
Assert.Equal(1, newLink.Position);
}
Assert.False(result.IsError);
var newLink = await Database.PlayListHasTracks.SingleAsync(link => link.TrackId == newTrack.Id, TestContext.Current.CancellationToken);
Assert.Equal(1, newLink.Position);
}
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(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 playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(stranger.Id, playList.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_TrackMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.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_TrackAlreadyInPlayList_ReturnsConflict()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
[Fact]
public async Task Handle_TrackAlreadyInPlayList_ReturnsConflict()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
@@ -6,109 +6,108 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
{
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private CreatePlayListCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<CreatePlayListCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.PlayList(),
TestConfigurations.UploadIntent());
[Fact]
public async Task Handle_NoPictureIntent_CreatesPlayListWithoutPictures()
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
var user = TestEntities.User();
await SeedAsync(user);
private CreatePlayListCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<CreatePlayListCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.PlayList(),
TestConfigurations.UploadIntent());
var command = new CreatePlayListCommand(user.Id, "My Playlist", "A description", PictureIntentId: null);
[Fact]
public async Task Handle_NoPictureIntent_CreatesPlayListWithoutPictures()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreatePlayListCommand(user.Id, "My Playlist", "A description", PictureIntentId: null);
Assert.False(result.IsError);
Assert.Equal("My Playlist", result.Value.Name);
Assert.Null(result.Value.SmallImageKeyName);
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_WithValidPictureIntent_PublishesResourcesEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
Assert.False(result.IsError);
Assert.Equal("My Playlist", result.Value.Name);
Assert.Null(result.Value.SmallImageKeyName);
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
}
var user = TestEntities.User();
var intent = TestEntities.UploadIntent(user.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "cover.webp");
await SeedAsync(user, intent);
[Fact]
public async Task Handle_WithValidPictureIntent_PublishesResourcesEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, intent.Id);
var user = TestEntities.User();
var intent = TestEntities.UploadIntent(user.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "cover.webp");
await SeedAsync(user, intent);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, intent.Id);
Assert.False(result.IsError);
var stored = await Database.PlayLists.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoVisibilitySpecified_DefaultsToPrivate()
{
var user = TestEntities.User();
await SeedAsync(user);
Assert.False(result.IsError);
var stored = await Database.PlayLists.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
}
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null);
[Fact]
public async Task Handle_NoVisibilitySpecified_DefaultsToPrivate()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Private, result.Value.Visibility);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PublicVisibility_PersistsIt()
{
var user = TestEntities.User();
await SeedAsync(user);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Private, result.Value.Visibility);
}
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
[Fact]
public async Task Handle_PublicVisibility_PersistsIt()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new CreatePlayListCommand(404, "Orphan Playlist", null, null);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new CreatePlayListCommand(404, "Orphan Playlist", null, null);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PictureIntentNotFound_ReturnsError()
{
var user = TestEntities.User();
await SeedAsync(user);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, Guid.NewGuid());
[Fact]
public async Task Handle_PictureIntentNotFound_ReturnsError()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, Guid.NewGuid());
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.True(result.IsError);
}
Assert.True(result.IsError);
}
}
@@ -6,49 +6,48 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
{
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private DeletePlayListCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeletePlayListCommandHandler>());
[Fact]
public async Task Handle_Owner_MarksPlayListAsRemoving()
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
private DeletePlayListCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeletePlayListCommandHandler>());
var result = await CreateHandler().Handle(new DeletePlayListCommand(owner.Id, playList.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_Owner_MarksPlayListAsRemoving()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
Assert.False(result.IsError);
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeletePlayListEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(new DeletePlayListCommand(owner.Id, playList.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeletePlayListCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeletePlayListEvent>(), Arg.Any<CancellationToken>());
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeletePlayListCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new DeletePlayListCommand(stranger.Id, playList.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
var result = await CreateHandler().Handle(new DeletePlayListCommand(stranger.Id, playList.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
}
@@ -4,82 +4,81 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
private GetPlayListByIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds()
{
private GetPlayListByIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
[Fact]
public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(playList.Name, result.Value.Name);
Assert.Equal([track.Id], result.Value.CoverTrackIds);
}
Assert.False(result.IsError);
Assert.Equal(playList.Name, result.Value.Name);
Assert.Equal([track.Id], result.Value.CoverTrackIds);
}
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_PrivatePlayList_RequestedByOwner_ReturnsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
await SeedAsync(owner, playList);
[Fact]
public async Task Handle_PrivatePlayList_RequestedByOwner_ReturnsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, owner.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
}
Assert.False(result.IsError);
}
[Fact]
public async Task Handle_PrivatePlayList_RequestedByStranger_ReturnsNotFound()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
await SeedAsync(owner, stranger, playList);
[Fact]
public async Task Handle_PrivatePlayList_RequestedByStranger_ReturnsNotFound()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
await SeedAsync(owner, stranger, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, stranger.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, stranger.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_PrivatePlayList_RequestedAnonymously_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
await SeedAsync(owner, playList);
[Fact]
public async Task Handle_PrivatePlayList_RequestedAnonymously_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_PublicPlayList_RequestedAnonymously_ReturnsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
await SeedAsync(owner, playList);
[Fact]
public async Task Handle_PublicPlayList_RequestedAnonymously_ReturnsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
}
Assert.False(result.IsError);
}
}
@@ -3,49 +3,48 @@ using Musify.Application.PlayLists;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
{
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
private GetPlayListCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.PlayList());
[Theory]
[InlineData("small", "small.webp")]
[InlineData("large", "large.webp")]
[InlineData("medium", "medium.webp")]
public async Task Handle_ExistingPlayList_ReturnsRequestedSize(string size, string expectedFileName)
{
private GetPlayListCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.PlayList());
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
[Theory]
[InlineData("small", "small.webp")]
[InlineData("large", "large.webp")]
[InlineData("medium", "medium.webp")]
public async Task Handle_ExistingPlayList_ReturnsRequestedSize(string size, string expectedFileName)
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, size), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.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_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_PlayListWithoutPictures_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
playList.Pictures = null;
await SeedAsync(owner, playList);
[Fact]
public async Task Handle_PlayListWithoutPictures_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
playList.Pictures = null;
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, "small"), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, "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.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
{
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
private GetPlayListTracksQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_TracksOrderedByPosition_ReturnsThemInOrder()
{
private GetPlayListTracksQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var second = TestEntities.Track(owner, "Second");
var first = TestEntities.Track(owner, "First");
await SeedAsync(
owner, playList, second, first,
new PlayListHasTrack { PlayListId = playList.Id, TrackId = second.Id, Position = 1 },
new PlayListHasTrack { PlayListId = playList.Id, TrackId = first.Id, Position = 0 });
[Fact]
public async Task Handle_TracksOrderedByPosition_ReturnsThemInOrder()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var second = TestEntities.Track(owner, "Second");
var first = TestEntities.Track(owner, "First");
await SeedAsync(
owner, playList, second, first,
new PlayListHasTrack { PlayListId = playList.Id, TrackId = second.Id, Position = 1 },
new PlayListHasTrack { PlayListId = playList.Id, TrackId = first.Id, Position = 0 });
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.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_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -3,57 +3,56 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
private GetPlayListsByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ReturnsOnlyActivePlayListsForThatUser()
{
private GetPlayListsByUserIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
await SeedAsync(
owner, other,
TestEntities.PlayList(owner.Id, "Mine"),
TestEntities.PlayList(owner.Id, "Being removed", lifeCycleStatus: LifeCycleStatus.Removing),
TestEntities.PlayList(other.Id, "Theirs"));
[Fact]
public async Task Handle_ReturnsOnlyActivePlayListsForThatUser()
{
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
await SeedAsync(
owner, other,
TestEntities.PlayList(owner.Id, "Mine"),
TestEntities.PlayList(owner.Id, "Being removed", lifeCycleStatus: LifeCycleStatus.Removing),
TestEntities.PlayList(other.Id, "Theirs"));
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Mine", playList.Name);
}
Assert.False(result.IsError);
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Mine", playList.Name);
}
[Fact]
public async Task Handle_OnlyPublic_ReturnsOnlyPublicPlayLists()
{
var owner = TestEntities.User();
await SeedAsync(
owner,
TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public),
TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private));
[Fact]
public async Task Handle_OnlyPublic_ReturnsOnlyPublicPlayLists()
{
var owner = TestEntities.User();
await SeedAsync(
owner,
TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public),
TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private));
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, OnlyPublic: true), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, OnlyPublic: true), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Public one", playList.Name);
}
Assert.False(result.IsError);
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Public one", playList.Name);
}
[Fact]
public async Task Handle_NameFilter_ReturnsOnlyMatchingPlayLists()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "Road Trip"), TestEntities.PlayList(owner.Id, "Study Focus"));
[Fact]
public async Task Handle_NameFilter_ReturnsOnlyMatchingPlayLists()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "Road Trip"), TestEntities.PlayList(owner.Id, "Study Focus"));
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: "road", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: "road", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Road Trip", playList.Name);
}
Assert.False(result.IsError);
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Road Trip", playList.Name);
}
}
@@ -2,31 +2,30 @@ using Musify.Application.PlayLists;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
{
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
private GetPlayListsQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ReturnsEveryPlayListPaged()
{
private GetPlayListsQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "One"), TestEntities.PlayList(owner.Id, "Two"));
[Fact]
public async Task Handle_ReturnsEveryPlayListPaged()
{
var owner = TestEntities.User();
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "One"), TestEntities.PlayList(owner.Id, "Two"));
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
}
[Fact]
public async Task Handle_NoPlayLists_ReturnsEmptyPage()
{
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoPlayLists_ReturnsEmptyPage()
{
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
}
@@ -4,57 +4,56 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
{
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
private RemoveTrackFromPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<RemoveTrackFromPlayListCommandHandler>());
[Fact]
public async Task Handle_TrackInPlayList_RemovesTheLink()
{
private RemoveTrackFromPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<RemoveTrackFromPlayListCommandHandler>());
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
[Fact]
public async Task Handle_TrackInPlayList_RemovesTheLink()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(Database.PlayListHasTracks);
}
Assert.False(result.IsError);
Assert.Empty(Database.PlayListHasTracks);
}
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(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 playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(stranger.Id, playList.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_TrackNotLinked_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
[Fact]
public async Task Handle_TrackNotLinked_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.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.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
{
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private RequestPlayListPictureUploadCommandHandler 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 RequestPlayListPictureUploadCommandHandler 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 RequestPlayListPictureUploadCommandHandler(
Database,
new UploadIntentValidator(Database, storageService),
storageService,
NoOpLogger<RequestPlayListPictureUploadCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.PlayList(),
uploadIntentConfig ?? TestConfigurations.UploadIntent());
}
return new RequestPlayListPictureUploadCommandHandler(
Database,
new UploadIntentValidator(Database, storageService),
storageService,
NoOpLogger<RequestPlayListPictureUploadCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.PlayList(),
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 RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp");
var command = new RequestPlayListPictureUploadCommand(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 RequestPlayListPictureUploadCommand(404, "webp", "image/webp");
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new RequestPlayListPictureUploadCommand(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 RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
var command = new RequestPlayListPictureUploadCommand(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,131 +6,130 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists
namespace Musify.Application.Tests.PlayLists;
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
{
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private UpdatePlayListCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<UpdatePlayListCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.PlayList(),
TestConfigurations.UploadIntent());
[Fact]
public async Task Handle_Owner_RenamesPlayList()
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, "Old Name");
await SeedAsync(owner, playList);
private UpdatePlayListCommandHandler CreateHandler() => new(
eventBus,
Database,
new UploadIntentValidator(Database, storageService),
NoOpLogger<UpdatePlayListCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.PlayList(),
TestConfigurations.UploadIntent());
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
[Fact]
public async Task Handle_Owner_RenamesPlayList()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, "Old Name");
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
Assert.False(result.IsError);
Assert.Equal("New Name", result.Value.Name);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", null, null);
Assert.False(result.IsError);
Assert.Equal("New Name", result.Value.Name);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListMissing_ReturnsNotFound()
{
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", 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 playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, null);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, stranger, playList);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, null);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NewVisibility_UpdatesIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlaylistVisibility.Public);
[Fact]
public async Task Handle_NewVisibility_UpdatesIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlaylistVisibility.Public);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
await SeedAsync(owner, playList);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
}
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
[Fact]
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
await SeedAsync(owner, playList);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
}
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " ", " ", null);
[Fact]
public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " ", " ", null);
Assert.False(result.IsError);
Assert.Equal("Original Name", result.Value.Name);
Assert.Equal("Original description", result.Value.Description);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
Assert.False(result.IsError);
Assert.Equal("Original Name", result.Value.Name);
Assert.Equal("Original description", result.Value.Description);
}
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
playList.Pictures = null;
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
await SeedAsync(owner, playList, intent);
[Fact]
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
playList.Pictures = null;
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
await SeedAsync(owner, playList, intent);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
var stored = await Database.PlayLists.FindAsync([playList.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.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
}
Assert.False(result.IsError);
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
var stored = await Database.PlayLists.FindAsync([playList.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.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
}
}
@@ -6,94 +6,93 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services
namespace Musify.Application.Tests.Services;
public sealed class TrackStreamIssuerTests : HandlerTestBase
{
public sealed class TrackStreamIssuerTests : HandlerTestBase
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
private TrackStreamIssuer CreateIssuer(PlaybackConfiguration? playback = null) => new(
Database,
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test/"),
playback ?? TestConfigurations.Playback());
[Fact]
public async Task IssueAsync_BuildsManifestUrlAndRecordsListeningHistory()
{
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
ticketService.IssueTicket(7, Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("signed-token", 120));
var owner = TestEntities.User(7);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
private TrackStreamIssuer CreateIssuer(PlaybackConfiguration? playback = null) => new(
Database,
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test/"),
playback ?? TestConfigurations.Playback());
var response = await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 7, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_BuildsManifestUrlAndRecordsListeningHistory()
{
ticketService.IssueTicket(7, Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("signed-token", 120));
var owner = TestEntities.User(7);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
Assert.Equal("signed-token", response.Ticket);
Assert.Equal(120, response.ExpiresInSeconds);
Assert.Equal(
"https://stream.musify.test/media/Tracks/ProcessedAudios/folder-42/audio.m4a",
response.ManifestUrl);
var response = await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 7, TestContext.Current.CancellationToken);
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(7, history.UserId);
Assert.Equal(track.Id, history.TrackId);
}
Assert.Equal("signed-token", response.Ticket);
Assert.Equal(120, response.ExpiresInSeconds);
Assert.Equal(
"https://stream.musify.test/media/Tracks/ProcessedAudios/folder-42/audio.m4a",
response.ManifestUrl);
[Fact]
public async Task IssueAsync_PassesTheProcessedAudioKeyPrefixToTheTicketService()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(7, history.UserId);
Assert.Equal(track.Id, history.TrackId);
}
await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_PassesTheProcessedAudioKeyPrefixToTheTicketService()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_AnonymousUser_SkipsListeningHistoryAndNeverThrows()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
var response = await CreateIssuer(TestConfigurations.Playback(allowAnonymousListening: true))
.IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_AnonymousUser_SkipsListeningHistoryAndNeverThrows()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
Assert.Equal("token", response.Ticket);
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
}
var response = await CreateIssuer(TestConfigurations.Playback(allowAnonymousListening: true))
.IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_AnonymousUserWithFragmentConfigured_PassesTheByteCapToTheTicketService()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
Assert.Equal("token", response.Ticket);
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
}
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_AnonymousUserWithFragmentConfigured_PassesTheByteCapToTheTicketService()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
ticketService.Received(1).IssueTicket(null, "Tracks/ProcessedAudios/folder-42/", 30L * playback.EstimatedAudioBytesPerSecond);
}
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_AuthenticatedUser_IgnoresTheAnonymousFragmentCap()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
ticketService.Received(1).IssueTicket(null, "Tracks/ProcessedAudios/folder-42/", 30L * playback.EstimatedAudioBytesPerSecond);
}
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
[Fact]
public async Task IssueAsync_AuthenticatedUser_IgnoresTheAnonymousFragmentCap()
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
var owner = TestEntities.User(1);
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
}
}
@@ -6,141 +6,140 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services
namespace Musify.Application.Tests.Services;
public sealed class UploadIntentValidatorTests : HandlerTestBase
{
public sealed class UploadIntentValidatorTests : HandlerTestBase
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private readonly Domain.Entities.User owner = TestEntities.User();
private UploadIntentValidator CreateValidator() => new(Database, storageService);
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectWithinLimits_ReturnsTheIntent()
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private readonly Domain.Entities.User owner = TestEntities.User();
storageService
.HeadObjectAsync("bucket", "temp/object.webp", Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
private UploadIntentValidator CreateValidator() => new(Database, storageService);
var intent = TestEntities.UploadIntent(owner.Id, bucket: "bucket", key: "temp/object.webp");
await SeedAsync(owner, intent);
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectWithinLimits_ReturnsTheIntent()
{
storageService
.HeadObjectAsync("bucket", "temp/object.webp", Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
var intent = TestEntities.UploadIntent(owner.Id, bucket: "bucket", key: "temp/object.webp");
await SeedAsync(owner, intent);
Assert.False(result.IsError);
Assert.Equal(intent.Id, result.Value.Id);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_IntentMissing_ReturnsNotFound()
{
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), Guid.NewGuid(), owner.Id, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(intent.Id, result.Value.Id);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task ValidateAndLoadAsync_IntentMissing_ReturnsNotFound()
{
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), Guid.NewGuid(), owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_IntentBelongsToAnotherUser_ReturnsNotFound()
{
var stranger = TestEntities.User(2, "stranger");
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, stranger, intent);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, stranger.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_IntentBelongsToAnotherUser_ReturnsNotFound()
{
var stranger = TestEntities.User(2, "stranger");
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, stranger, intent);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, stranger.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_AlreadyConsumed_ReturnsConflict()
{
var intent = TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed);
await SeedAsync(owner, intent);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_AlreadyConsumed_ReturnsConflict()
{
var intent = TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed);
await SeedAsync(owner, intent);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_PastExpiry_ReturnsValidationError()
{
var intent = TestEntities.UploadIntent(owner.Id, expiresAt: DateTime.UtcNow.AddMinutes(-1));
await SeedAsync(owner, intent);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_PastExpiry_ReturnsValidationError()
{
var intent = TestEntities.UploadIntent(owner.Id, expiresAt: DateTime.UtcNow.AddMinutes(-1));
await SeedAsync(owner, intent);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, 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.Validation, result.FirstError.Type);
}
var intent = TestEntities.UploadIntent(owner.Id);
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(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, intent);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectExceedsMaxSize_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxUploadBytes = 1024;
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
[Fact]
public async Task ValidateAndLoadAsync_UploadedObjectExceedsMaxSize_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxUploadBytes = 1024;
var intent = TestEntities.UploadIntent(owner.Id);
await SeedAsync(owner, intent);
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 2048));
var result = await CreateValidator().ValidateAndLoadAsync(config, intent.Id, owner.Id, 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(config, intent.Id, owner.Id, TestContext.Current.CancellationToken);
[Fact]
public async Task CheckQuotaAsync_WithinLimits_ReturnsSuccess()
{
await SeedAsync(owner);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var result = await CreateValidator().CheckQuotaAsync(TestConfigurations.UploadIntent(), owner.Id, requiredBytes: 1024, requiredIntentCount: 1, TestContext.Current.CancellationToken);
[Fact]
public async Task CheckQuotaAsync_WithinLimits_ReturnsSuccess()
{
await SeedAsync(owner);
Assert.False(result.IsError);
}
var result = await CreateValidator().CheckQuotaAsync(TestConfigurations.UploadIntent(), owner.Id, requiredBytes: 1024, requiredIntentCount: 1, TestContext.Current.CancellationToken);
[Fact]
public async Task CheckQuotaAsync_TooManyActiveIntents_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
await SeedAsync(owner, TestEntities.UploadIntent(owner.Id));
Assert.False(result.IsError);
}
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
[Fact]
public async Task CheckQuotaAsync_TooManyActiveIntents_ReturnsValidationError()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
await SeedAsync(owner, TestEntities.UploadIntent(owner.Id));
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
[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));
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
[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().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
}
Assert.False(result.IsError);
}
}
@@ -1,41 +1,40 @@
using Musify.Application.Shared;
using Xunit;
namespace Musify.Application.Tests.Shared
namespace Musify.Application.Tests.Shared;
public sealed class StorageKeyTests
{
public sealed class StorageKeyTests
[Fact]
public void Combine_MultipleSegments_JoinsThemWithForwardSlashes()
{
[Fact]
public void Combine_MultipleSegments_JoinsThemWithForwardSlashes()
{
var key = StorageKey.Combine("Tracks", "SmallPictures", "cover.webp");
var key = StorageKey.Combine("Tracks", "SmallPictures", "cover.webp");
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
}
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
}
[Fact]
public void Combine_SegmentsWithSurroundingSlashesAndWhitespace_TrimsThem()
{
var key = StorageKey.Combine(" Tracks/ ", "/SmallPictures/", "cover.webp");
[Fact]
public void Combine_SegmentsWithSurroundingSlashesAndWhitespace_TrimsThem()
{
var key = StorageKey.Combine(" Tracks/ ", "/SmallPictures/", "cover.webp");
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
}
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Combine_BlankSegment_IsSkipped(string? blank)
{
var key = StorageKey.Combine("Tracks", blank!, "cover.webp");
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Combine_BlankSegment_IsSkipped(string? blank)
{
var key = StorageKey.Combine("Tracks", blank!, "cover.webp");
Assert.Equal("Tracks/cover.webp", key);
}
Assert.Equal("Tracks/cover.webp", key);
}
[Fact]
public void Combine_NoSegments_ReturnsEmptyString()
{
Assert.Equal(string.Empty, StorageKey.Combine());
}
[Fact]
public void Combine_NoSegments_ReturnsEmptyString()
{
Assert.Equal(string.Empty, StorageKey.Combine());
}
}
@@ -2,32 +2,31 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks.Responses;
using Xunit;
namespace Musify.Application.Tests.Shared
namespace Musify.Application.Tests.Shared;
public sealed class TrackApplicationResponseTests
{
public sealed class TrackApplicationResponseTests
[Fact]
public void FromEntity_UsesOwnerNameAsArtist()
{
[Fact]
public void FromEntity_UsesOwnerNameAsArtist()
{
var owner = TestEntities.User(1, "The Owner");
var track = TestEntities.Track(owner);
var owner = TestEntities.User(1, "The Owner");
var track = TestEntities.Track(owner);
var response = TrackApplicationResponse.FromEntity(track, listensCount: 3);
var response = TrackApplicationResponse.FromEntity(track, listensCount: 3);
Assert.Equal("The Owner", response.Artist);
Assert.Equal(owner.Id, response.OwnerUserId);
Assert.Equal(3, response.ListensCount);
Assert.False(response.IsExplicit);
}
Assert.Equal("The Owner", response.Artist);
Assert.Equal(owner.Id, response.OwnerUserId);
Assert.Equal(3, response.ListensCount);
Assert.False(response.IsExplicit);
}
[Fact]
public void FromEntity_CopiesExplicitFlag()
{
var track = TestEntities.Track(TestEntities.User(), isExplicit: true);
[Fact]
public void FromEntity_CopiesExplicitFlag()
{
var track = TestEntities.Track(TestEntities.User(), isExplicit: true);
var response = TrackApplicationResponse.FromEntity(track, listensCount: 0);
var response = TrackApplicationResponse.FromEntity(track, listensCount: 0);
Assert.True(response.IsExplicit);
}
Assert.True(response.IsExplicit);
}
}
@@ -2,26 +2,25 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Musify.Application.Tests.TestSupport
namespace Musify.Application.Tests.TestSupport;
public abstract class HandlerTestBase : IAsyncLifetime
{
public abstract class HandlerTestBase : IAsyncLifetime
private TestDatabase database = null!;
protected TestDatabase Database => database;
public async ValueTask InitializeAsync() => database = await TestDatabase.CreateAsync();
public ValueTask DisposeAsync() => database.DisposeAsync();
protected static ILogger<T> NoOpLogger<T>() => NullLogger<T>.Instance;
protected async Task SeedAsync(params object[] entities)
{
private TestDatabase database = null!;
foreach (var entity in entities)
Database.Add(entity);
protected TestDatabase Database => database;
public async ValueTask InitializeAsync() => database = await TestDatabase.CreateAsync();
public ValueTask DisposeAsync() => database.DisposeAsync();
protected static ILogger<T> NoOpLogger<T>() => NullLogger<T>.Instance;
protected async Task SeedAsync(params object[] entities)
{
foreach (var entity in entities)
Database.Add(entity);
await Database.SaveChangesAsync(CancellationToken.None);
}
await Database.SaveChangesAsync(CancellationToken.None);
}
}
@@ -1,26 +1,25 @@
using Musify.Application.Configuration;
namespace Musify.Application.Tests.TestSupport
namespace Musify.Application.Tests.TestSupport;
public static class TestConfigurations
{
public static class TestConfigurations
{
public static ApplicationStorageConfiguration Storage(string bucket = "test-bucket") =>
new() { Bucket = bucket };
public static ApplicationStorageConfiguration Storage(string bucket = "test-bucket") =>
new() { Bucket = bucket };
public static TrackConfiguration Track() => new();
public static TrackConfiguration Track() => new();
public static PlayListConfiguration PlayList() => new();
public static PlayListConfiguration PlayList() => new();
public static AlbumConfiguration Album() => new();
public static AlbumConfiguration Album() => new();
public static MixConfiguration Mix() => new();
public static MixConfiguration Mix() => new();
public static UploadIntentConfiguration UploadIntent() => new();
public static UploadIntentConfiguration UploadIntent() => new();
public static StreamGatewayConfiguration StreamGateway(string publicBaseUrl = "https://stream.musify.test") =>
new() { PublicBaseUrl = publicBaseUrl };
public static StreamGatewayConfiguration StreamGateway(string publicBaseUrl = "https://stream.musify.test") =>
new() { PublicBaseUrl = publicBaseUrl };
public static PlaybackConfiguration Playback(bool allowAnonymousListening = false, int anonymousFragmentSeconds = 0) =>
new() { AllowAnonymousListening = allowAnonymousListening, AnonymousFragmentSeconds = anonymousFragmentSeconds };
}
public static PlaybackConfiguration Playback(bool allowAnonymousListening = false, int anonymousFragmentSeconds = 0) =>
new() { AllowAnonymousListening = allowAnonymousListening, AnonymousFragmentSeconds = anonymousFragmentSeconds };
}
@@ -3,180 +3,179 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
using Musify.Domain.Entities;
namespace Musify.Application.Tests.TestSupport
namespace Musify.Application.Tests.TestSupport;
public sealed class TestDatabase : DbContext, IDatabase
{
public sealed class TestDatabase : DbContext, IDatabase
private readonly SqliteConnection connection;
private TestDatabase(SqliteConnection connection, DbContextOptions<TestDatabase> options)
: base(options)
{
private readonly SqliteConnection connection;
this.connection = connection;
}
private TestDatabase(SqliteConnection connection, DbContextOptions<TestDatabase> options)
: base(options)
public static async Task<TestDatabase> CreateAsync()
{
var connection = new SqliteConnection("Filename=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<TestDatabase>()
.UseSqlite(connection)
.Options;
var database = new TestDatabase(connection, options);
await database.Database.EnsureCreatedAsync();
return database;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Track>().OwnsOne(track => track.Pictures);
modelBuilder.Entity<Track>().Navigation(track => track.Pictures).IsRequired();
modelBuilder.Entity<Track>().OwnsOne(track => track.Audio);
modelBuilder.Entity<Track>().Navigation(track => track.Audio).IsRequired();
modelBuilder.Entity<Track>()
.HasOne(track => track.Owner)
.WithMany()
.HasForeignKey(track => track.OwnerUserId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<PlayList>().OwnsOne(playList => playList.Pictures, pictures =>
{
this.connection = connection;
}
pictures.Property(p => p.OriginalName).IsRequired(false);
pictures.Property(p => p.SmallName).IsRequired(false);
pictures.Property(p => p.MediumName).IsRequired(false);
pictures.Property(p => p.LargeName).IsRequired(false);
});
modelBuilder.Entity<PlayList>().Navigation(playList => playList.Pictures).IsRequired(false);
public static async Task<TestDatabase> CreateAsync()
{
var connection = new SqliteConnection("Filename=:memory:");
await connection.OpenAsync();
modelBuilder.Entity<Album>().OwnsOne(album => album.Pictures);
modelBuilder.Entity<Album>().Navigation(album => album.Pictures).IsRequired(false);
var options = new DbContextOptionsBuilder<TestDatabase>()
.UseSqlite(connection)
.Options;
modelBuilder.Entity<Album>()
.HasOne(album => album.Owner)
.WithMany()
.HasForeignKey(album => album.OwnerUserId)
.OnDelete(DeleteBehavior.Restrict);
var database = new TestDatabase(connection, options);
await database.Database.EnsureCreatedAsync();
return database;
}
modelBuilder.Entity<AlbumHasTrack>()
.HasOne(albumTrack => albumTrack.Album)
.WithMany(album => album.AlbumTracks)
.HasForeignKey(albumTrack => albumTrack.AlbumId)
.OnDelete(DeleteBehavior.Cascade);
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<AlbumHasTrack>()
.HasOne(albumTrack => albumTrack.Track)
.WithMany(track => track.AlbumTracks)
.HasForeignKey(albumTrack => albumTrack.TrackId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Track>().OwnsOne(track => track.Pictures);
modelBuilder.Entity<Track>().Navigation(track => track.Pictures).IsRequired();
modelBuilder.Entity<AlbumHasTrack>()
.HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId })
.IsUnique();
modelBuilder.Entity<Track>().OwnsOne(track => track.Audio);
modelBuilder.Entity<Track>().Navigation(track => track.Audio).IsRequired();
modelBuilder.Entity<Mix>()
.HasIndex(mix => new { mix.UserId, mix.Position });
modelBuilder.Entity<Track>()
.HasOne(track => track.Owner)
.WithMany()
.HasForeignKey(track => track.OwnerUserId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<MixItem>()
.HasOne(item => item.Mix)
.WithMany(mix => mix.Items)
.HasForeignKey(item => item.MixId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<PlayList>().OwnsOne(playList => playList.Pictures, pictures =>
{
pictures.Property(p => p.OriginalName).IsRequired(false);
pictures.Property(p => p.SmallName).IsRequired(false);
pictures.Property(p => p.MediumName).IsRequired(false);
pictures.Property(p => p.LargeName).IsRequired(false);
});
modelBuilder.Entity<PlayList>().Navigation(playList => playList.Pictures).IsRequired(false);
modelBuilder.Entity<MixItem>()
.HasOne(item => item.Track)
.WithMany()
.HasForeignKey(item => item.TrackId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Album>().OwnsOne(album => album.Pictures);
modelBuilder.Entity<Album>().Navigation(album => album.Pictures).IsRequired(false);
modelBuilder.Entity<MixItem>()
.HasIndex(item => new { item.MixId, item.Position });
modelBuilder.Entity<Album>()
.HasOne(album => album.Owner)
.WithMany()
.HasForeignKey(album => album.OwnerUserId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<TrackLike>()
.HasIndex(like => new { like.UserId, like.TrackId })
.IsUnique();
modelBuilder.Entity<AlbumHasTrack>()
.HasOne(albumTrack => albumTrack.Album)
.WithMany(album => album.AlbumTracks)
.HasForeignKey(albumTrack => albumTrack.AlbumId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<TrackTag>()
.HasOne(trackTag => trackTag.Track)
.WithMany(track => track.Tags)
.HasForeignKey(trackTag => trackTag.TrackId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<AlbumHasTrack>()
.HasOne(albumTrack => albumTrack.Track)
.WithMany(track => track.AlbumTracks)
.HasForeignKey(albumTrack => albumTrack.TrackId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<TrackTag>()
.HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag })
.IsUnique();
modelBuilder.Entity<AlbumHasTrack>()
.HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId })
.IsUnique();
modelBuilder.Entity<UserFollow>()
.HasOne(follow => follow.Follower)
.WithMany()
.HasForeignKey(follow => follow.FollowerId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Mix>()
.HasIndex(mix => new { mix.UserId, mix.Position });
modelBuilder.Entity<UserFollow>()
.HasOne(follow => follow.Followed)
.WithMany()
.HasForeignKey(follow => follow.FollowedId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<MixItem>()
.HasOne(item => item.Mix)
.WithMany(mix => mix.Items)
.HasForeignKey(item => item.MixId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UserFollow>()
.HasIndex(follow => new { follow.FollowerId, follow.FollowedId })
.IsUnique();
}
modelBuilder.Entity<MixItem>()
.HasOne(item => item.Track)
.WithMany()
.HasForeignKey(item => item.TrackId)
.OnDelete(DeleteBehavior.Cascade);
public DbSet<User> Users => Set<User>();
modelBuilder.Entity<MixItem>()
.HasIndex(item => new { item.MixId, item.Position });
public DbSet<PlayList> PlayLists => Set<PlayList>();
modelBuilder.Entity<TrackLike>()
.HasIndex(like => new { like.UserId, like.TrackId })
.IsUnique();
public DbSet<Album> Albums => Set<Album>();
modelBuilder.Entity<TrackTag>()
.HasOne(trackTag => trackTag.Track)
.WithMany(track => track.Tags)
.HasForeignKey(trackTag => trackTag.TrackId)
.OnDelete(DeleteBehavior.Cascade);
public DbSet<AlbumHasTrack> AlbumHasTracks => Set<AlbumHasTrack>();
modelBuilder.Entity<TrackTag>()
.HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag })
.IsUnique();
public DbSet<Track> Tracks => Set<Track>();
modelBuilder.Entity<UserFollow>()
.HasOne(follow => follow.Follower)
.WithMany()
.HasForeignKey(follow => follow.FollowerId)
.OnDelete(DeleteBehavior.Restrict);
public DbSet<UserHasTrack> UserHasTracks => Set<UserHasTrack>();
modelBuilder.Entity<UserFollow>()
.HasOne(follow => follow.Followed)
.WithMany()
.HasForeignKey(follow => follow.FollowedId)
.OnDelete(DeleteBehavior.Restrict);
public DbSet<PlayListHasTrack> PlayListHasTracks => Set<PlayListHasTrack>();
modelBuilder.Entity<UserFollow>()
.HasIndex(follow => new { follow.FollowerId, follow.FollowedId })
.IsUnique();
}
public DbSet<Mix> Mixes => Set<Mix>();
public DbSet<User> Users => Set<User>();
public DbSet<MixItem> MixItems => Set<MixItem>();
public DbSet<PlayList> PlayLists => Set<PlayList>();
public DbSet<Upload> Uploads => Set<Upload>();
public DbSet<Album> Albums => Set<Album>();
public DbSet<UploadIntent> UploadIntents => Set<UploadIntent>();
public DbSet<AlbumHasTrack> AlbumHasTracks => Set<AlbumHasTrack>();
public DbSet<ListeningHistory> ListeningHistories => Set<ListeningHistory>();
public DbSet<Track> Tracks => Set<Track>();
public DbSet<TrackLike> TrackLikes => Set<TrackLike>();
public DbSet<UserHasTrack> UserHasTracks => Set<UserHasTrack>();
public DbSet<TrackTag> TrackTags => Set<TrackTag>();
public DbSet<PlayListHasTrack> PlayListHasTracks => Set<PlayListHasTrack>();
public DbSet<UserFollow> UserFollows => Set<UserFollow>();
public DbSet<Mix> Mixes => Set<Mix>();
public async Task<IDatabaseTransaction> BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken)
{
var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken);
return new SqliteDatabaseTransaction(transaction);
}
public DbSet<MixItem> MixItems => Set<MixItem>();
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
await connection.DisposeAsync();
}
public DbSet<Upload> Uploads => Set<Upload>();
private sealed class SqliteDatabaseTransaction(Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction inner)
: IDatabaseTransaction
{
public Task CommitAsync(CancellationToken cancellationToken = default) =>
inner.CommitAsync(cancellationToken);
public DbSet<UploadIntent> UploadIntents => Set<UploadIntent>();
public DbSet<ListeningHistory> ListeningHistories => Set<ListeningHistory>();
public DbSet<TrackLike> TrackLikes => Set<TrackLike>();
public DbSet<TrackTag> TrackTags => Set<TrackTag>();
public DbSet<UserFollow> UserFollows => Set<UserFollow>();
public async Task<IDatabaseTransaction> BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken)
{
var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken);
return new SqliteDatabaseTransaction(transaction);
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
await connection.DisposeAsync();
}
private sealed class SqliteDatabaseTransaction(Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction inner)
: IDatabaseTransaction
{
public Task CommitAsync(CancellationToken cancellationToken = default) =>
inner.CommitAsync(cancellationToken);
public ValueTask DisposeAsync() => inner.DisposeAsync();
}
public ValueTask DisposeAsync() => inner.DisposeAsync();
}
}
@@ -1,194 +1,193 @@
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.Tests.TestSupport
namespace Musify.Application.Tests.TestSupport;
public static class TestEntities
{
public static class TestEntities
public static User User(
long id = 1,
string name = "test-user",
string? firstName = "Test",
string? secondName = "User",
string? profilePictureUrl = null) =>
new()
{
Id = id,
Name = name,
NormalizedName = name.ToUpperInvariant(),
FirstName = firstName,
SecondName = secondName,
ProfilePictureUrl = profilePictureUrl
};
public static TrackPictures ProcessedPictures() => new()
{
public static User User(
long id = 1,
string name = "test-user",
string? firstName = "Test",
string? secondName = "User",
string? profilePictureUrl = null) =>
new()
{
Id = id,
Name = name,
NormalizedName = name.ToUpperInvariant(),
FirstName = firstName,
SecondName = secondName,
ProfilePictureUrl = profilePictureUrl
};
OriginalName = "original.webp",
SmallName = "small.webp",
MediumName = "medium.webp",
LargeName = "large.webp",
ProcessingStatus = ProcessingStatus.Completed
};
public static TrackPictures ProcessedPictures() => new()
public static TrackPictures PendingPictures() => new()
{
OriginalName = "original.webp",
ProcessingStatus = ProcessingStatus.Pending
};
public static TrackAudio ProcessedAudio(string folderName = "audio-folder") => new()
{
OriginalName = "original.mp3",
FolderName = folderName,
TranscodeStatus = ProcessingStatus.Completed
};
public static TrackAudio PendingAudio() => new()
{
OriginalName = "original.mp3",
TranscodeStatus = ProcessingStatus.Pending
};
public static Track Track(
User owner,
string title = "Test Track",
double durationSeconds = 180,
TrackPictures? pictures = null,
TrackAudio? audio = null,
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
IEnumerable<Genre>? tags = null,
bool isExplicit = false)
{
var track = new Track
{
OriginalName = "original.webp",
SmallName = "small.webp",
MediumName = "medium.webp",
LargeName = "large.webp",
ProcessingStatus = ProcessingStatus.Completed
Title = title,
NormalizedTitle = title.ToUpperInvariant(),
DurationSeconds = durationSeconds,
LifeCycleStatus = lifeCycleStatus,
IsExplicit = isExplicit,
OwnerUserId = owner.Id,
Owner = owner,
Pictures = pictures ?? ProcessedPictures(),
Audio = audio ?? ProcessedAudio()
};
public static TrackPictures PendingPictures() => new()
{
OriginalName = "original.webp",
ProcessingStatus = ProcessingStatus.Pending
};
track.Tags = (tags ?? [Genre.Pop])
.Select(tag => new TrackTag { TrackId = track.Id, Tag = tag })
.ToList();
public static TrackAudio ProcessedAudio(string folderName = "audio-folder") => new()
{
OriginalName = "original.mp3",
FolderName = folderName,
TranscodeStatus = ProcessingStatus.Completed
};
public static TrackAudio PendingAudio() => new()
{
OriginalName = "original.mp3",
TranscodeStatus = ProcessingStatus.Pending
};
public static Track Track(
User owner,
string title = "Test Track",
double durationSeconds = 180,
TrackPictures? pictures = null,
TrackAudio? audio = null,
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
IEnumerable<Genre>? tags = null,
bool isExplicit = false)
{
var track = new Track
{
Title = title,
NormalizedTitle = title.ToUpperInvariant(),
DurationSeconds = durationSeconds,
LifeCycleStatus = lifeCycleStatus,
IsExplicit = isExplicit,
OwnerUserId = owner.Id,
Owner = owner,
Pictures = pictures ?? ProcessedPictures(),
Audio = audio ?? ProcessedAudio()
};
track.Tags = (tags ?? [Genre.Pop])
.Select(tag => new TrackTag { TrackId = track.Id, Tag = tag })
.ToList();
return track;
}
public static PlayListPictures PlayListPictures() => new()
{
OriginalName = "original.webp",
SmallName = "small.webp",
MediumName = "medium.webp",
LargeName = "large.webp"
};
public static PlayList PlayList(
long userId,
string name = "Test Playlist",
string? description = null,
PlayListPictures? pictures = null,
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
PlaylistVisibility visibility = PlaylistVisibility.Private) =>
new()
{
UserId = userId,
Name = name,
NormalizedName = name.ToUpperInvariant(),
Description = description,
Pictures = pictures ?? PlayListPictures(),
LifeCycleStatus = lifeCycleStatus,
Visibility = visibility
};
public static AlbumPictures AlbumPictures() => new()
{
OriginalName = "original.webp",
SmallName = "small.webp",
MediumName = "medium.webp",
LargeName = "large.webp"
};
public static Album Album(
long ownerUserId,
string title = "Test Album",
string? description = null,
int? releaseYear = 2024,
AlbumPictures? pictures = null) =>
new()
{
Title = title,
NormalizedTitle = title.ToUpperInvariant(),
Description = description,
ReleaseYear = releaseYear,
OwnerUserId = ownerUserId,
Pictures = pictures ?? AlbumPictures()
};
public static UploadIntent UploadIntent(
long userId,
UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture,
string bucket = "test-bucket",
string key = "temp/object.webp",
string objectName = "object.webp",
string contentType = "image/webp",
long? expectedSizeBytes = 1024,
UploadIntentStatus status = UploadIntentStatus.Issued,
DateTime? expiresAt = null) =>
new()
{
UserId = userId,
Bucket = bucket,
Key = key,
ObjectName = objectName,
ContentType = contentType,
ExpectedSizeBytes = expectedSizeBytes,
Purpose = purpose,
Status = status,
ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5)
};
public static Mix Mix(
long userId,
string title = "Test Mix",
string? subtitle = null,
int position = 0) =>
new()
{
UserId = userId,
Title = title,
Subtitle = subtitle,
Position = position
};
public static MixItem MixItem(
Guid mixId,
Guid trackId,
int position = 0) =>
new()
{
MixId = mixId,
Position = position,
TrackId = trackId
};
public static ListeningHistory ListeningHistory(
long userId,
Guid trackId,
DateTime? listenedAt = null,
double? playedSeconds = null,
bool isCounted = true) =>
new()
{
UserId = userId,
TrackId = trackId,
ListenedAt = listenedAt ?? DateTime.UtcNow,
PlayedSeconds = playedSeconds,
IsCounted = isCounted
};
return track;
}
public static PlayListPictures PlayListPictures() => new()
{
OriginalName = "original.webp",
SmallName = "small.webp",
MediumName = "medium.webp",
LargeName = "large.webp"
};
public static PlayList PlayList(
long userId,
string name = "Test Playlist",
string? description = null,
PlayListPictures? pictures = null,
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
PlaylistVisibility visibility = PlaylistVisibility.Private) =>
new()
{
UserId = userId,
Name = name,
NormalizedName = name.ToUpperInvariant(),
Description = description,
Pictures = pictures ?? PlayListPictures(),
LifeCycleStatus = lifeCycleStatus,
Visibility = visibility
};
public static AlbumPictures AlbumPictures() => new()
{
OriginalName = "original.webp",
SmallName = "small.webp",
MediumName = "medium.webp",
LargeName = "large.webp"
};
public static Album Album(
long ownerUserId,
string title = "Test Album",
string? description = null,
int? releaseYear = 2024,
AlbumPictures? pictures = null) =>
new()
{
Title = title,
NormalizedTitle = title.ToUpperInvariant(),
Description = description,
ReleaseYear = releaseYear,
OwnerUserId = ownerUserId,
Pictures = pictures ?? AlbumPictures()
};
public static UploadIntent UploadIntent(
long userId,
UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture,
string bucket = "test-bucket",
string key = "temp/object.webp",
string objectName = "object.webp",
string contentType = "image/webp",
long? expectedSizeBytes = 1024,
UploadIntentStatus status = UploadIntentStatus.Issued,
DateTime? expiresAt = null) =>
new()
{
UserId = userId,
Bucket = bucket,
Key = key,
ObjectName = objectName,
ContentType = contentType,
ExpectedSizeBytes = expectedSizeBytes,
Purpose = purpose,
Status = status,
ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5)
};
public static Mix Mix(
long userId,
string title = "Test Mix",
string? subtitle = null,
int position = 0) =>
new()
{
UserId = userId,
Title = title,
Subtitle = subtitle,
Position = position
};
public static MixItem MixItem(
Guid mixId,
Guid trackId,
int position = 0) =>
new()
{
MixId = mixId,
Position = position,
TrackId = trackId
};
public static ListeningHistory ListeningHistory(
long userId,
Guid trackId,
DateTime? listenedAt = null,
double? playedSeconds = null,
bool isCounted = true) =>
new()
{
UserId = userId,
TrackId = trackId,
ListenedAt = listenedAt ?? DateTime.UtcNow,
PlayedSeconds = playedSeconds,
IsCounted = isCounted
};
}
@@ -8,143 +8,142 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
{
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private CreateTrackCommandHandler CreateHandler()
{
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 CreateTrackCommandHandler CreateHandler()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
return new CreateTrackCommandHandler(
Database,
eventBus,
new UploadIntentValidator(Database, storageService),
NoOpLogger<CreateTrackCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Track(),
TestConfigurations.UploadIntent());
}
return new CreateTrackCommandHandler(
Database,
eventBus,
new UploadIntentValidator(Database, storageService),
NoOpLogger<CreateTrackCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Track(),
TestConfigurations.UploadIntent());
}
private static (Domain.Entities.UploadIntent Picture, Domain.Entities.UploadIntent Audio) SeedIntents(long userId) =>
(TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "cover.webp"),
TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3"));
private static (Domain.Entities.UploadIntent Picture, Domain.Entities.UploadIntent Audio) SeedIntents(long userId) =>
(TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "cover.webp"),
TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3"));
[Fact]
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
[Fact]
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop, Genre.Rock]);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop, Genre.Rock]);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("My Song", result.Value.Title);
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
Assert.Equal(user.Id, localResponse.OwnerUserId);
Assert.Equal([Genre.Pop, Genre.Rock], localResponse.Tags);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
Assert.False(result.IsError);
Assert.Equal("My Song", result.Value.Title);
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
Assert.Equal(user.Id, localResponse.OwnerUserId);
Assert.Equal([Genre.Pop, Genre.Rock], localResponse.Tags);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
}
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Handle_StoresExplicitFlag(bool isExplicit)
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Handle_StoresExplicitFlag(bool isExplicit)
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(isExplicit, result.Value.IsExplicit);
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(isExplicit, stored.IsExplicit);
}
Assert.False(result.IsError);
Assert.Equal(isExplicit, result.Value.IsExplicit);
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(isExplicit, stored.IsExplicit);
}
[Fact]
public async Task Handle_NoTags_ReturnsValidationError()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
[Fact]
public async Task Handle_NoTags_ReturnsValidationError()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, []);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, []);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task Handle_IncompatibleTags_ReturnsValidationError()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
[Fact]
public async Task Handle_IncompatibleTags_ReturnsValidationError()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Classical, Genre.Metal]);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Classical, Genre.Metal]);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop]);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop]);
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_PictureIntentBelongsToAnotherUser_ReturnsError()
{
var user = TestEntities.User(1, "user");
var otherUser = TestEntities.User(2, "other");
var (picture, audio) = SeedIntents(otherUser.Id);
await SeedAsync(user, otherUser, picture, audio);
[Fact]
public async Task Handle_PictureIntentBelongsToAnotherUser_ReturnsError()
{
var user = TestEntities.User(1, "user");
var otherUser = TestEntities.User(2, "other");
var (picture, audio) = SeedIntents(otherUser.Id);
await SeedAsync(user, otherUser, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.True(result.IsError);
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
}
Assert.True(result.IsError);
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_AudioIntentAlreadyConsumed_ReturnsError()
{
var user = TestEntities.User();
var picture = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "cover.webp");
var audio = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", status: UploadIntentStatus.Consumed);
await SeedAsync(user, picture, audio);
[Fact]
public async Task Handle_AudioIntentAlreadyConsumed_ReturnsError()
{
var user = TestEntities.User();
var picture = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "cover.webp");
var audio = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", status: UploadIntentStatus.Consumed);
await SeedAsync(user, picture, audio);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
@@ -3,30 +3,29 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
{
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
[Fact]
public async Task Handle_DeletesOnlyOldUncountedListens()
{
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
var oldUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 2, isCounted: false);
var oldCounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 100);
var recentUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-2), playedSeconds: 2, isCounted: false);
await SeedAsync(owner, track, oldUncounted, oldCounted, recentUncounted);
[Fact]
public async Task Handle_DeletesOnlyOldUncountedListens()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
var oldUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 2, isCounted: false);
var oldCounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 100);
var recentUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-2), playedSeconds: 2, isCounted: false);
await SeedAsync(owner, track, oldUncounted, oldCounted, recentUncounted);
var deleted = await CreateHandler().Handle(new DeleteStaleUncountedListensCommand(), TestContext.Current.CancellationToken);
var deleted = await CreateHandler().Handle(new DeleteStaleUncountedListensCommand(), TestContext.Current.CancellationToken);
Assert.Equal(1, deleted);
var remaining = await Database.ListeningHistories.Select(l => l.Id).ToListAsync(TestContext.Current.CancellationToken);
Assert.DoesNotContain(oldUncounted.Id, remaining);
Assert.Contains(oldCounted.Id, remaining);
Assert.Contains(recentUncounted.Id, remaining);
}
Assert.Equal(1, deleted);
var remaining = await Database.ListeningHistories.Select(l => l.Id).ToListAsync(TestContext.Current.CancellationToken);
Assert.DoesNotContain(oldUncounted.Id, remaining);
Assert.Contains(oldCounted.Id, remaining);
Assert.Contains(recentUncounted.Id, remaining);
}
}
@@ -6,61 +6,60 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
{
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
[Fact]
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
Assert.False(result.IsError);
var stored = await Database.Tracks.FindAsync([track.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeleteTrackEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var stored = await Database.Tracks.FindAsync([track.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeleteTrackEvent>(), Arg.Any<CancellationToken>());
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, track);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new DeleteTrackCommand(stranger.Id, track.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, track);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new DeleteTrackCommand(stranger.Id, track.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AudioStillProcessing_ReturnsConflict()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
await SeedAsync(owner, track);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AudioStillProcessing_ReturnsConflict()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
@@ -3,38 +3,37 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetLastTrackListenedByUserIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetLastTrackListenedByUserIdQueryHandlerTests : HandlerTestBase
private GetLastTrackListenedByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_MultipleListens_ReturnsMostRecentlyListenedTrack()
{
private GetLastTrackListenedByUserIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var older = TestEntities.Track(owner, "Older listen");
var newer = TestEntities.Track(owner, "Newer listen");
await SeedAsync(
owner, older, newer,
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
[Fact]
public async Task Handle_MultipleListens_ReturnsMostRecentlyListenedTrack()
{
var owner = TestEntities.User();
var older = TestEntities.Track(owner, "Older listen");
var newer = TestEntities.Track(owner, "Newer listen");
await SeedAsync(
owner, older, newer,
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("Newer listen", result.Value.Title);
}
Assert.False(result.IsError);
Assert.Equal("Newer listen", result.Value.Title);
}
[Fact]
public async Task Handle_NoHistory_ReturnsNotFound()
{
var owner = TestEntities.User();
await SeedAsync(owner);
[Fact]
public async Task Handle_NoHistory_ReturnsNotFound()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -2,54 +2,53 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
{
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_MultipleListens_ReturnsDistinctTracksMostRecentFirst()
{
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var older = TestEntities.Track(owner, "Older listen");
var newer = TestEntities.Track(owner, "Newer listen");
await SeedAsync(
owner, older, newer,
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-5)),
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
[Fact]
public async Task Handle_MultipleListens_ReturnsDistinctTracksMostRecentFirst()
{
var owner = TestEntities.User();
var older = TestEntities.Track(owner, "Older listen");
var newer = TestEntities.Track(owner, "Newer listen");
await SeedAsync(
owner, older, newer,
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-5)),
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
}
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
}
[Fact]
public async Task Handle_NoHistory_ReturnsEmpty()
{
var owner = TestEntities.User();
await SeedAsync(owner);
[Fact]
public async Task Handle_NoHistory_ReturnsEmpty()
{
var owner = TestEntities.User();
await SeedAsync(owner);
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.Empty(result);
}
Assert.Empty(result);
}
[Fact]
public async Task Handle_UncountedListens_AreIgnored()
{
var owner = TestEntities.User();
var counted = TestEntities.Track(owner, "Counted");
var skipped = TestEntities.Track(owner, "Skipped");
await SeedAsync(
owner, counted, skipped,
TestEntities.ListeningHistory(owner.Id, counted.Id, DateTime.UtcNow.AddMinutes(-5), playedSeconds: 90),
TestEntities.ListeningHistory(owner.Id, skipped.Id, DateTime.UtcNow, playedSeconds: 2, isCounted: false));
[Fact]
public async Task Handle_UncountedListens_AreIgnored()
{
var owner = TestEntities.User();
var counted = TestEntities.Track(owner, "Counted");
var skipped = TestEntities.Track(owner, "Skipped");
await SeedAsync(
owner, counted, skipped,
TestEntities.ListeningHistory(owner.Id, counted.Id, DateTime.UtcNow.AddMinutes(-5), playedSeconds: 90),
TestEntities.ListeningHistory(owner.Id, skipped.Id, DateTime.UtcNow, playedSeconds: 2, isCounted: false));
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.Equal(["Counted"], result.Select(track => track.Title));
}
Assert.Equal(["Counted"], result.Select(track => track.Title));
}
}
@@ -4,50 +4,49 @@ using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_LocalTrack_ReturnsItWithOwnerAsArtist()
{
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User(1, "artist-name");
var track = TestEntities.Track(owner, "My Song");
await SeedAsync(owner, track);
[Fact]
public async Task Handle_LocalTrack_ReturnsItWithOwnerAsArtist()
{
var owner = TestEntities.User(1, "artist-name");
var track = TestEntities.Track(owner, "My Song");
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("My Song", result.Value.Title);
Assert.Equal(owner.Name, result.Value.Artist);
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
Assert.Equal(owner.Id, localResponse.OwnerUserId);
}
Assert.False(result.IsError);
Assert.Equal("My Song", result.Value.Title);
Assert.Equal(owner.Name, result.Value.Artist);
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
Assert.Equal(owner.Id, localResponse.OwnerUserId);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_ListensCount_OnlyCountsCountedListens()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(
owner, track,
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 1, isCounted: false));
[Fact]
public async Task Handle_ListensCount_OnlyCountsCountedListens()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(
owner, track,
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 1, isCounted: false));
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
Assert.Equal(2, result.Value.ListensCount);
}
Assert.Equal(2, result.Value.ListensCount);
}
}
@@ -3,50 +3,49 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
{
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
private GetTrackCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Track());
[Theory]
[InlineData("small", "small.webp")]
[InlineData("medium", "medium.webp")]
[InlineData("large", "large.webp")]
[InlineData("unknown-size-falls-back-to-medium", "medium.webp")]
public async Task Handle_ProcessedTrack_ReturnsRequestedSize(string size, string expectedFileName)
{
private GetTrackCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Track());
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Theory]
[InlineData("small", "small.webp")]
[InlineData("medium", "medium.webp")]
[InlineData("large", "large.webp")]
[InlineData("unknown-size-falls-back-to-medium", "medium.webp")]
public async Task Handle_ProcessedTrack_ReturnsRequestedSize(string size, string expectedFileName)
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, size), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, size), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("covers-bucket", result.Value.Bucket);
Assert.EndsWith(expectedFileName, result.Value.Key);
Assert.Equal("image/webp", result.Value.ContentType);
}
Assert.False(result.IsError);
Assert.Equal("covers-bucket", result.Value.Bucket);
Assert.EndsWith(expectedFileName, result.Value.Key);
Assert.Equal("image/webp", result.Value.ContentType);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_PictureNotProcessedYet_ReturnsNotFound()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, pictures: TestEntities.PendingPictures());
await SeedAsync(owner, track);
[Fact]
public async Task Handle_PictureNotProcessedYet_ReturnsNotFound()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, pictures: TestEntities.PendingPictures());
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, "small"), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, "small"), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -8,96 +8,95 @@ using Musify.Application.Tracks;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
{
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
private GetTrackStreamQueryHandler CreateHandler(PlaybackConfiguration? playback = null)
{
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("ticket-token", 60));
private GetTrackStreamQueryHandler CreateHandler(PlaybackConfiguration? playback = null)
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("ticket-token", 60));
var issuer = new TrackStreamIssuer(
Database,
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test"),
playback ?? TestConfigurations.Playback());
var issuer = new TrackStreamIssuer(
Database,
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test"),
playback ?? TestConfigurations.Playback());
return new GetTrackStreamQueryHandler(Database, issuer, playback ?? TestConfigurations.Playback(), NoOpLogger<GetTrackStreamQueryHandler>());
}
return new GetTrackStreamQueryHandler(Database, issuer, playback ?? TestConfigurations.Playback(), NoOpLogger<GetTrackStreamQueryHandler>());
}
[Fact]
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("ticket-token", result.Value.Ticket);
Assert.Equal(60, result.Value.ExpiresInSeconds);
Assert.Contains("audio-folder", result.Value.ManifestUrl);
Assert.False(result.IsError);
Assert.Equal("ticket-token", result.Value.Ticket);
Assert.Equal(60, result.Value.ExpiresInSeconds);
Assert.Contains("audio-folder", result.Value.ManifestUrl);
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(owner.Id, history.UserId);
Assert.Equal(track.Id, history.TrackId);
Assert.Equal(history.Id, result.Value.ListenId);
Assert.False(history.IsCounted);
Assert.Null(history.PlayedSeconds);
}
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(owner.Id, history.UserId);
Assert.Equal(track.Id, history.TrackId);
Assert.Equal(history.Id, result.Value.ListenId);
Assert.False(history.IsCounted);
Assert.Null(history.PlayedSeconds);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackStreamQuery(Guid.NewGuid(), 1), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackStreamQuery(Guid.NewGuid(), 1), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
await SeedAsync(owner, track);
[Fact]
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
await SeedAsync(owner, track);
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningDisabled_ReturnsUnauthorized()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningDisabled_ReturnsUnauthorized()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: false));
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: false));
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningEnabled_IssuesTicketWithoutRecordingHistory()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningEnabled_IssuesTicketWithoutRecordingHistory()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: true));
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: true));
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("ticket-token", result.Value.Ticket);
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Equal("ticket-token", result.Value.Ticket);
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
}
}
@@ -3,44 +3,43 @@ using Musify.Application.Tracks;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ReturnsOnlyTracksLinkedToThatUser()
{
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
var mine = TestEntities.Track(owner, "Mine");
var theirs = TestEntities.Track(other, "Theirs");
await SeedAsync(
owner, other, mine, theirs,
new UserHasTrack { UserId = owner.Id, TrackId = mine.Id },
new UserHasTrack { UserId = other.Id, TrackId = theirs.Id });
[Fact]
public async Task Handle_ReturnsOnlyTracksLinkedToThatUser()
{
var owner = TestEntities.User(1, "owner");
var other = TestEntities.User(2, "other");
var mine = TestEntities.Track(owner, "Mine");
var theirs = TestEntities.Track(other, "Theirs");
await SeedAsync(
owner, other, mine, theirs,
new UserHasTrack { UserId = owner.Id, TrackId = mine.Id },
new UserHasTrack { UserId = other.Id, TrackId = theirs.Id });
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var track = Assert.Single(result.Value.Items);
Assert.Equal("Mine", track.Title);
}
Assert.False(result.IsError);
var track = Assert.Single(result.Value.Items);
Assert.Equal("Mine", track.Title);
}
[Theory]
[InlineData(0, 1)]
[InlineData(-5, 1)]
public async Task Handle_NonPositivePageNumber_FallsBackToFirstPage(int requestedPageNumber, int expectedPageNumber)
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track, new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
[Theory]
[InlineData(0, 1)]
[InlineData(-5, 1)]
public async Task Handle_NonPositivePageNumber_FallsBackToFirstPage(int requestedPageNumber, int expectedPageNumber)
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track, new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: requestedPageNumber, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: requestedPageNumber, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(expectedPageNumber, result.Value.PageNumber);
}
Assert.False(result.IsError);
Assert.Equal(expectedPageNumber, result.Value.PageNumber);
}
}
@@ -3,64 +3,63 @@ using Musify.Application.Tracks;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
{
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
private GetTracksQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_NoNameFilter_ReturnsEveryTrackAUserOwns()
{
private GetTracksQueryHandler CreateHandler() => new(Database);
var owner = TestEntities.User();
var trackA = TestEntities.Track(owner, "Alpha");
var trackB = TestEntities.Track(owner, "Beta");
await SeedAsync(
owner, trackA, trackB,
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackA.Id },
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackB.Id });
[Fact]
public async Task Handle_NoNameFilter_ReturnsEveryTrackAUserOwns()
{
var owner = TestEntities.User();
var trackA = TestEntities.Track(owner, "Alpha");
var trackB = TestEntities.Track(owner, "Beta");
await SeedAsync(
owner, trackA, trackB,
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackA.Id },
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackB.Id });
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.Equal(2, result.Value.Items.Count);
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.Equal(2, result.Value.Items.Count);
}
[Fact]
public async Task Handle_NameFilter_ReturnsOnlyMatchingTracks()
{
var owner = TestEntities.User();
var match = TestEntities.Track(owner, "Bohemian Rhapsody");
var other = TestEntities.Track(owner, "Imagine");
await SeedAsync(
owner, match, other,
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = match.Id },
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = other.Id });
[Fact]
public async Task Handle_NameFilter_ReturnsOnlyMatchingTracks()
{
var owner = TestEntities.User();
var match = TestEntities.Track(owner, "Bohemian Rhapsody");
var other = TestEntities.Track(owner, "Imagine");
await SeedAsync(
owner, match, other,
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = match.Id },
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = other.Id });
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal("Bohemian Rhapsody", item.Track.Title);
}
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal("Bohemian Rhapsody", item.Track.Title);
}
[Fact]
public async Task Handle_GenreFilter_ReturnsOnlyTracksWithThatTag()
{
var owner = TestEntities.User();
var rock = TestEntities.Track(owner, "Loud", tags: [Genre.Rock, Genre.Indie]);
var jazz = TestEntities.Track(owner, "Smooth", tags: [Genre.Jazz]);
await SeedAsync(
owner, rock, jazz,
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = rock.Id },
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
[Fact]
public async Task Handle_GenreFilter_ReturnsOnlyTracksWithThatTag()
{
var owner = TestEntities.User();
var rock = TestEntities.Track(owner, "Loud", tags: [Genre.Rock, Genre.Indie]);
var jazz = TestEntities.Track(owner, "Smooth", tags: [Genre.Jazz]);
await SeedAsync(
owner, rock, jazz,
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = rock.Id },
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal("Loud", item.Track.Title);
}
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal("Loud", item.Track.Title);
}
}
@@ -5,123 +5,122 @@ using Musify.Application.Tracks;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
{
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
private RecordListeningProgressCommandHandler CreateHandler() => new(Database);
private async Task<ListeningHistory> ReloadAsync(Guid id) =>
await Database.ListeningHistories.AsNoTracking()
.SingleAsync(l => l.Id == id, TestContext.Current.CancellationToken);
private async Task<(User User, ListeningHistory Listen)> SeedListenAsync(
double durationSeconds, DateTime listenedAt)
{
private RecordListeningProgressCommandHandler CreateHandler() => new(Database);
var user = TestEntities.User();
var track = TestEntities.Track(user, durationSeconds: durationSeconds);
var listen = TestEntities.ListeningHistory(user.Id, track.Id, listenedAt, isCounted: false);
await SeedAsync(user, track, listen);
return (user, listen);
}
private async Task<ListeningHistory> ReloadAsync(Guid id) =>
await Database.ListeningHistories.AsNoTracking()
.SingleAsync(l => l.Id == id, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
private async Task<(User User, ListeningHistory Listen)> SeedListenAsync(
double durationSeconds, DateTime listenedAt)
{
var user = TestEntities.User();
var track = TestEntities.Track(user, durationSeconds: durationSeconds);
var listen = TestEntities.ListeningHistory(user.Id, track.Id, listenedAt, isCounted: false);
await SeedAsync(user, track, listen);
return (user, listen);
}
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
Assert.False(result.IsError);
var stored = await ReloadAsync(listen.Id);
Assert.Equal(10, stored.PlayedSeconds);
Assert.False(stored.IsCounted);
}
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
Assert.False(result.IsError);
var stored = await ReloadAsync(listen.Id);
Assert.Equal(10, stored.PlayedSeconds);
Assert.False(stored.IsCounted);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ShortTrack_CountsAtHalfDuration()
{
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ShortTrack_CountsAtHalfDuration()
{
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_LowerValueThanStored_KeepsHighest()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var handler = CreateHandler();
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 40), TestContext.Current.CancellationToken);
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 15), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_LowerValueThanStored_KeepsHighest()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var handler = CreateHandler();
var stored = await ReloadAsync(listen.Id);
Assert.Equal(40, stored.PlayedSeconds);
Assert.True(stored.IsCounted);
}
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 40), TestContext.Current.CancellationToken);
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 15), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
var stored = await ReloadAsync(listen.Id);
Assert.Equal(40, stored.PlayedSeconds);
Assert.True(stored.IsCounted);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
var stored = await ReloadAsync(listen.Id);
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
{
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
var stored = await ReloadAsync(listen.Id);
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
{
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
}
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_OtherUsersListen_ReturnsNotFound()
{
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
}
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_OtherUsersListen_ReturnsNotFound()
{
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
Assert.True(result.IsError);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
[Theory]
[InlineData(-1)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public async Task Handle_InvalidSeconds_ReturnsValidationError(double seconds)
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
Assert.True(result.IsError);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, seconds), TestContext.Current.CancellationToken);
[Theory]
[InlineData(-1)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public async Task Handle_InvalidSeconds_ReturnsValidationError(double seconds)
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, seconds), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
}
@@ -9,73 +9,72 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks
namespace Musify.Application.Tests.Tracks;
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
{
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private RequestTrackUploadUrlsCommandHandler 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 RequestTrackUploadUrlsCommandHandler 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 RequestTrackUploadUrlsCommandHandler(
Database,
new UploadIntentValidator(Database, storageService),
storageService,
NoOpLogger<RequestTrackUploadUrlsCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Track(),
uploadIntentConfig ?? TestConfigurations.UploadIntent());
}
return new RequestTrackUploadUrlsCommandHandler(
Database,
new UploadIntentValidator(Database, storageService),
storageService,
NoOpLogger<RequestTrackUploadUrlsCommandHandler>(),
TestConfigurations.Storage(),
TestConfigurations.Track(),
uploadIntentConfig ?? TestConfigurations.UploadIntent());
}
[Fact]
public async Task Handle_UserExists_IssuesBothUploadUrlsAndCreatesIntents()
{
var user = TestEntities.User();
await SeedAsync(user);
[Fact]
public async Task Handle_UserExists_IssuesBothUploadUrlsAndCreatesIntents()
{
var user = TestEntities.User();
await SeedAsync(user);
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
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.PictureUploadUrl);
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.AudioUploadUrl);
Assert.False(result.IsError);
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.PictureUploadUrl);
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.AudioUploadUrl);
var intents = await Database.UploadIntents.Where(intent => intent.UserId == user.Id).ToListAsync(TestContext.Current.CancellationToken);
Assert.Equal(2, intents.Count);
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackPicture);
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackAudio);
}
var intents = await Database.UploadIntents.Where(intent => intent.UserId == user.Id).ToListAsync(TestContext.Current.CancellationToken);
Assert.Equal(2, intents.Count);
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackPicture);
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackAudio);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new RequestTrackUploadUrlsCommand(404, "webp", "image/webp", "mp3", "audio/mpeg");
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new RequestTrackUploadUrlsCommand(404, "webp", "image/webp", "mp3", "audio/mpeg");
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_UserAtActiveIntentLimit_ReturnsValidationErrorAndCreatesNothing()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
[Fact]
public async Task Handle_UserAtActiveIntentLimit_ReturnsValidationErrorAndCreatesNothing()
{
var config = TestConfigurations.UploadIntent();
config.MaxActiveUploadIntentsPerUser = 1;
var user = TestEntities.User();
await SeedAsync(user, TestEntities.UploadIntent(user.Id));
var user = TestEntities.User();
await SeedAsync(user, TestEntities.UploadIntent(user.Id));
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
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.Equal(1, await Database.UploadIntents.CountAsync(TestContext.Current.CancellationToken));
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
Assert.Equal(1, await Database.UploadIntents.CountAsync(TestContext.Current.CancellationToken));
}
}
@@ -3,39 +3,38 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
{
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
private CreateUserCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateUserCommandHandler>());
[Fact]
public async Task Handle_NewId_CreatesUser()
{
private CreateUserCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateUserCommandHandler>());
var command = new CreateUserCommand(1, "Jane Doe", "Jane", "Doe");
[Fact]
public async Task Handle_NewId_CreatesUser()
{
var command = new CreateUserCommand(1, "Jane Doe", "Jane", "Doe");
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("Jane Doe", result.Value.Name);
Assert.Equal(1, result.Value.Id);
Assert.False(result.IsError);
Assert.Equal("Jane Doe", result.Value.Name);
Assert.Equal(1, result.Value.Id);
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("JANE DOE", stored.NormalizedName);
}
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("JANE DOE", stored.NormalizedName);
}
[Fact]
public async Task Handle_IdAlreadyExists_ReturnsConflict()
{
var existing = TestEntities.User(1);
await SeedAsync(existing);
[Fact]
public async Task Handle_IdAlreadyExists_ReturnsConflict()
{
var existing = TestEntities.User(1);
await SeedAsync(existing);
var command = new CreateUserCommand(1, "Someone Else", null, null);
var command = new CreateUserCommand(1, "Someone Else", null, null);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
@@ -4,58 +4,57 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class FollowUserCommandHandlerTests : HandlerTestBase
{
public sealed class FollowUserCommandHandlerTests : HandlerTestBase
private FollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<FollowUserCommandHandler>());
[Fact]
public async Task Handle_ValidTarget_CreatesFollow()
{
private FollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<FollowUserCommandHandler>());
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed);
[Fact]
public async Task Handle_ValidTarget_CreatesFollow()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed);
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_AlreadyFollowing_IsIdempotent()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed, new Musify.Domain.Entities.UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
[Fact]
public async Task Handle_AlreadyFollowing_IsIdempotent()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed, new Musify.Domain.Entities.UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_SelfFollow_ReturnsValidationError()
{
var user = TestEntities.User();
await SeedAsync(user);
[Fact]
public async Task Handle_SelfFollow_ReturnsValidationError()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(new FollowUserCommand(user.Id, user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new FollowUserCommand(user.Id, user.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
[Fact]
public async Task Handle_TargetMissing_ReturnsNotFound()
{
var follower = TestEntities.User();
await SeedAsync(follower);
[Fact]
public async Task Handle_TargetMissing_ReturnsNotFound()
{
var follower = TestEntities.User();
await SeedAsync(follower);
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, 404), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, 404), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -2,117 +2,116 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class GetListeningStatsQueryHandlerTests : HandlerTestBase
{
public sealed class GetListeningStatsQueryHandlerTests : HandlerTestBase
private GetListeningStatsQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_NoHistory_ReturnsZeroes()
{
private GetListeningStatsQueryHandler CreateHandler() => new(Database);
var user = TestEntities.User();
await SeedAsync(user);
[Fact]
public async Task Handle_NoHistory_ReturnsZeroes()
{
var user = TestEntities.User();
await SeedAsync(user);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(0, result.TracksThisWeek);
Assert.Equal(0, result.SecondsThisWeek);
Assert.Equal(0, result.StreakDays);
}
Assert.Equal(0, result.TracksThisWeek);
Assert.Equal(0, result.SecondsThisWeek);
Assert.Equal(0, result.StreakDays);
}
[Fact]
public async Task Handle_ListensThisWeek_CountsDistinctTracksAndTotalSeconds()
{
var user = TestEntities.User();
var trackA = TestEntities.Track(user, "Track A", durationSeconds: 200);
var trackB = TestEntities.Track(user, "Track B", durationSeconds: 100);
await SeedAsync(
user, trackA, trackB,
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddDays(-1)),
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddHours(-1)),
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow),
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow.AddDays(-10)));
[Fact]
public async Task Handle_ListensThisWeek_CountsDistinctTracksAndTotalSeconds()
{
var user = TestEntities.User();
var trackA = TestEntities.Track(user, "Track A", durationSeconds: 200);
var trackB = TestEntities.Track(user, "Track B", durationSeconds: 100);
await SeedAsync(
user, trackA, trackB,
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddDays(-1)),
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddHours(-1)),
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow),
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow.AddDays(-10)));
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(2, result.TracksThisWeek);
Assert.Equal(500, result.SecondsThisWeek);
}
Assert.Equal(2, result.TracksThisWeek);
Assert.Equal(500, result.SecondsThisWeek);
}
[Fact]
public async Task Handle_PlayedSeconds_SumsPlayedTimeInsteadOfDuration()
{
var user = TestEntities.User();
var track = TestEntities.Track(user, durationSeconds: 200);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 200),
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 12, isCounted: false));
[Fact]
public async Task Handle_PlayedSeconds_SumsPlayedTimeInsteadOfDuration()
{
var user = TestEntities.User();
var track = TestEntities.Track(user, durationSeconds: 200);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 200),
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 12, isCounted: false));
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(212, result.SecondsThisWeek);
}
Assert.Equal(212, result.SecondsThisWeek);
}
[Fact]
public async Task Handle_UncountedListens_AreExcludedFromTracksAndStreak()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 3, isCounted: false));
[Fact]
public async Task Handle_UncountedListens_AreExcludedFromTracksAndStreak()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 3, isCounted: false));
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(0, result.TracksThisWeek);
Assert.Equal(0, result.StreakDays);
Assert.Equal(3, result.SecondsThisWeek);
}
Assert.Equal(0, result.TracksThisWeek);
Assert.Equal(0, result.StreakDays);
Assert.Equal(3, result.SecondsThisWeek);
}
[Fact]
public async Task Handle_ListenedTodayAndYesterday_StreakIsTwo()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
[Fact]
public async Task Handle_ListenedTodayAndYesterday_StreakIsTwo()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(2, result.StreakDays);
}
Assert.Equal(2, result.StreakDays);
}
[Fact]
public async Task Handle_GapBeforeToday_StopsStreakAtGap()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-2)));
[Fact]
public async Task Handle_GapBeforeToday_StopsStreakAtGap()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-2)));
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(1, result.StreakDays);
}
Assert.Equal(1, result.StreakDays);
}
[Fact]
public async Task Handle_NoListenToday_StreakIsZero()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
[Fact]
public async Task Handle_NoListenToday_StreakIsZero()
{
var user = TestEntities.User();
var track = TestEntities.Track(user);
await SeedAsync(
user, track,
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
Assert.Equal(0, result.StreakDays);
}
Assert.Equal(0, result.StreakDays);
}
}
@@ -3,30 +3,29 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
{
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
private GetUserByIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ExistingUser_ReturnsIt()
{
private GetUserByIdQueryHandler CreateHandler() => new(Database);
var user = TestEntities.User(1, "Jane Doe");
await SeedAsync(user);
[Fact]
public async Task Handle_ExistingUser_ReturnsIt()
{
var user = TestEntities.User(1, "Jane Doe");
await SeedAsync(user);
var result = await CreateHandler().Handle(new GetUserByIdQuery(1), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserByIdQuery(1), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("Jane Doe", result.Value.Name);
}
Assert.False(result.IsError);
Assert.Equal("Jane Doe", result.Value.Name);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetUserByIdQuery(404), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetUserByIdQuery(404), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -6,90 +6,89 @@ using Musify.Application.Users.Responses;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class GetUserFollowersQueryHandlerTests : HandlerTestBase
{
public sealed class GetUserFollowersQueryHandlerTests : HandlerTestBase
private GetUserFollowersQueryHandler CreateHandler() => new(Database);
private async Task<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Send(long? viewerId, long userId) =>
await CreateHandler().Handle(new GetUserFollowersQuery(viewerId, userId, 1, 20), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_OwnProfile_ReturnsFollowers()
{
private GetUserFollowersQueryHandler CreateHandler() => new(Database);
var user = TestEntities.User(1, "user");
var follower = TestEntities.User(2, "follower");
await SeedAsync(user, follower, new UserFollow { FollowerId = follower.Id, FollowedId = user.Id });
private async Task<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Send(long? viewerId, long userId) =>
await CreateHandler().Handle(new GetUserFollowersQuery(viewerId, userId, 1, 20), TestContext.Current.CancellationToken);
var result = await Send(user.Id, user.Id);
[Fact]
public async Task Handle_OwnProfile_ReturnsFollowers()
{
var user = TestEntities.User(1, "user");
var follower = TestEntities.User(2, "follower");
await SeedAsync(user, follower, new UserFollow { FollowerId = follower.Id, FollowedId = user.Id });
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal(follower.Id, item.Id);
Assert.False(item.IsFollowedByViewer);
}
var result = await Send(user.Id, user.Id);
[Fact]
public async Task Handle_MutualFollow_ReturnsFollowersWithViewerFollowState()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
var other = TestEntities.User(3, "other");
await SeedAsync(
viewer, user, other,
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id },
new UserFollow { FollowerId = other.Id, FollowedId = user.Id },
new UserFollow { FollowerId = viewer.Id, FollowedId = other.Id });
Assert.False(result.IsError);
var item = Assert.Single(result.Value.Items);
Assert.Equal(follower.Id, item.Id);
Assert.False(item.IsFollowedByViewer);
}
var result = await Send(viewer.Id, user.Id);
[Fact]
public async Task Handle_MutualFollow_ReturnsFollowersWithViewerFollowState()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
var other = TestEntities.User(3, "other");
await SeedAsync(
viewer, user, other,
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id },
new UserFollow { FollowerId = other.Id, FollowedId = user.Id },
new UserFollow { FollowerId = viewer.Id, FollowedId = other.Id });
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == other.Id, item.IsFollowedByViewer));
}
var result = await Send(viewer.Id, user.Id);
[Fact]
public async Task Handle_ViewerFollowsButNotFollowedBack_ReturnsForbidden()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == other.Id, item.IsFollowedByViewer));
}
var result = await Send(viewer.Id, user.Id);
[Fact]
public async Task Handle_ViewerFollowsButNotFollowedBack_ReturnsForbidden()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
var result = await Send(viewer.Id, user.Id);
[Fact]
public async Task Handle_FollowedByUserButNotFollowingBack_ReturnsForbidden()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(viewer, user, new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
var result = await Send(viewer.Id, user.Id);
[Fact]
public async Task Handle_FollowedByUserButNotFollowingBack_ReturnsForbidden()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(viewer, user, new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
var result = await Send(viewer.Id, user.Id);
[Fact]
public async Task Handle_Anonymous_ReturnsForbidden()
{
await SeedAsync(TestEntities.User(1, "user"));
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
var result = await Send(null, 1);
[Fact]
public async Task Handle_Anonymous_ReturnsForbidden()
{
await SeedAsync(TestEntities.User(1, "user"));
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
var result = await Send(null, 1);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await Send(1, 404);
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await Send(1, 404);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -4,72 +4,71 @@ using Musify.Application.Users;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class GetUserFollowingQueryHandlerTests : HandlerTestBase
{
public sealed class GetUserFollowingQueryHandlerTests : HandlerTestBase
private GetUserFollowingQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_Anonymous_ReturnsFollowedUsers()
{
private GetUserFollowingQueryHandler CreateHandler() => new(Database);
var user = TestEntities.User(1, "user");
var followedA = TestEntities.User(2, "followedA");
var followedB = TestEntities.User(3, "followedB");
await SeedAsync(
user, followedA, followedB,
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id });
[Fact]
public async Task Handle_Anonymous_ReturnsFollowedUsers()
{
var user = TestEntities.User(1, "user");
var followedA = TestEntities.User(2, "followedA");
var followedB = TestEntities.User(3, "followedB");
await SeedAsync(
user, followedA, followedB,
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id });
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, user.Id, 1, 20), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, user.Id, 1, 20), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.All(result.Value.Items, item => Assert.False(item.IsFollowedByViewer));
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
Assert.All(result.Value.Items, item => Assert.False(item.IsFollowedByViewer));
}
[Fact]
public async Task Handle_Viewer_MarksUsersTheViewerFollows()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
var followedA = TestEntities.User(3, "followedA");
var followedB = TestEntities.User(4, "followedB");
await SeedAsync(
viewer, user, followedA, followedB,
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id },
new UserFollow { FollowerId = viewer.Id, FollowedId = followedA.Id });
[Fact]
public async Task Handle_Viewer_MarksUsersTheViewerFollows()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
var followedA = TestEntities.User(3, "followedA");
var followedB = TestEntities.User(4, "followedB");
await SeedAsync(
viewer, user, followedA, followedB,
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id },
new UserFollow { FollowerId = viewer.Id, FollowedId = followedA.Id });
var result = await CreateHandler().Handle(new GetUserFollowingQuery(viewer.Id, user.Id, 1, 20), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserFollowingQuery(viewer.Id, user.Id, 1, 20), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == followedA.Id, item.IsFollowedByViewer));
}
Assert.False(result.IsError);
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == followedA.Id, item.IsFollowedByViewer));
}
[Fact]
public async Task Handle_Pagination_ReturnsRequestedPage()
{
await SeedAsync(
TestEntities.User(1, "user"), TestEntities.User(2, "a"), TestEntities.User(3, "b"), TestEntities.User(4, "c"),
new UserFollow { FollowerId = 1, FollowedId = 2 },
new UserFollow { FollowerId = 1, FollowedId = 3 },
new UserFollow { FollowerId = 1, FollowedId = 4 });
[Fact]
public async Task Handle_Pagination_ReturnsRequestedPage()
{
await SeedAsync(
TestEntities.User(1, "user"), TestEntities.User(2, "a"), TestEntities.User(3, "b"), TestEntities.User(4, "c"),
new UserFollow { FollowerId = 1, FollowedId = 2 },
new UserFollow { FollowerId = 1, FollowedId = 3 },
new UserFollow { FollowerId = 1, FollowedId = 4 });
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 1, 2, 2), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 1, 2, 2), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Single(result.Value.Items);
Assert.Equal(3, result.Value.TotalItemCount);
Assert.False(result.Value.HasNextPage);
}
Assert.False(result.IsError);
Assert.Single(result.Value.Items);
Assert.Equal(3, result.Value.TotalItemCount);
Assert.False(result.Value.HasNextPage);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 404, 1, 20), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 404, 1, 20), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -4,78 +4,77 @@ using Musify.Application.Users;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class GetUserProfileQueryHandlerTests : HandlerTestBase
{
public sealed class GetUserProfileQueryHandlerTests : HandlerTestBase
private GetUserProfileQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ExistingUser_ReturnsFollowerAndFollowingCounts()
{
private GetUserProfileQueryHandler CreateHandler() => new(Database);
var user = TestEntities.User(1, "user");
var followerA = TestEntities.User(2, "followerA");
var followerB = TestEntities.User(3, "followerB");
var followed = TestEntities.User(4, "followed");
await SeedAsync(
user, followerA, followerB, followed,
new UserFollow { FollowerId = followerA.Id, FollowedId = user.Id },
new UserFollow { FollowerId = followerB.Id, FollowedId = user.Id },
new UserFollow { FollowerId = user.Id, FollowedId = followed.Id });
[Fact]
public async Task Handle_ExistingUser_ReturnsFollowerAndFollowingCounts()
{
var user = TestEntities.User(1, "user");
var followerA = TestEntities.User(2, "followerA");
var followerB = TestEntities.User(3, "followerB");
var followed = TestEntities.User(4, "followed");
await SeedAsync(
user, followerA, followerB, followed,
new UserFollow { FollowerId = followerA.Id, FollowedId = user.Id },
new UserFollow { FollowerId = followerB.Id, FollowedId = user.Id },
new UserFollow { FollowerId = user.Id, FollowedId = followed.Id });
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.FollowersCount);
Assert.Equal(1, result.Value.FollowingCount);
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.FollowersCount);
Assert.Equal(1, result.Value.FollowingCount);
}
[Fact]
public async Task Handle_MutualFollow_ViewerCanSeeFollowersAndIsFollowing()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(
viewer, user,
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
[Fact]
public async Task Handle_MutualFollow_ViewerCanSeeFollowersAndIsFollowing()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(
viewer, user,
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
Assert.True(result.Value.IsFollowing);
Assert.True(result.Value.CanViewFollowers);
}
Assert.True(result.Value.IsFollowing);
Assert.True(result.Value.CanViewFollowers);
}
[Fact]
public async Task Handle_OneWayFollow_ViewerCannotSeeFollowers()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
[Fact]
public async Task Handle_OneWayFollow_ViewerCannotSeeFollowers()
{
var viewer = TestEntities.User(1, "viewer");
var user = TestEntities.User(2, "user");
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
Assert.True(result.Value.IsFollowing);
Assert.False(result.Value.CanViewFollowers);
}
Assert.True(result.Value.IsFollowing);
Assert.False(result.Value.CanViewFollowers);
}
[Fact]
public async Task Handle_OwnProfile_CanSeeFollowersButIsNotFollowing()
{
await SeedAsync(TestEntities.User(1, "user"));
[Fact]
public async Task Handle_OwnProfile_CanSeeFollowersButIsNotFollowing()
{
await SeedAsync(TestEntities.User(1, "user"));
var result = await CreateHandler().Handle(new GetUserProfileQuery(1, 1), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUserProfileQuery(1, 1), TestContext.Current.CancellationToken);
Assert.False(result.Value.IsFollowing);
Assert.True(result.Value.CanViewFollowers);
}
Assert.False(result.Value.IsFollowing);
Assert.True(result.Value.CanViewFollowers);
}
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetUserProfileQuery(404), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetUserProfileQuery(404), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -3,46 +3,45 @@ using Musify.Application.Users;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
{
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
private GetUsersQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_NoSearch_ReturnsEveryUser()
{
private GetUsersQueryHandler CreateHandler() => new(Database);
await SeedAsync(TestEntities.User(1, "alice"), TestEntities.User(2, "bob"));
[Fact]
public async Task Handle_NoSearch_ReturnsEveryUser()
{
await SeedAsync(TestEntities.User(1, "alice"), TestEntities.User(2, "bob"));
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
}
Assert.False(result.IsError);
Assert.Equal(2, result.Value.TotalItemCount);
}
[Fact]
public async Task Handle_UsernameSearch_ReturnsOnlyMatches()
{
await SeedAsync(TestEntities.User(1, "alice-wonderland"), TestEntities.User(2, "bob-builder"));
[Fact]
public async Task Handle_UsernameSearch_ReturnsOnlyMatches()
{
await SeedAsync(TestEntities.User(1, "alice-wonderland"), TestEntities.User(2, "bob-builder"));
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: "alice"), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: "alice"), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var user = Assert.Single(result.Value.Items);
Assert.Equal(1, user.Id);
}
Assert.False(result.IsError);
var user = Assert.Single(result.Value.Items);
Assert.Equal(1, user.Id);
}
[Fact]
public async Task Handle_Viewer_MarksFollowedUsers()
{
await SeedAsync(
TestEntities.User(1, "viewer"), TestEntities.User(2, "alice"), TestEntities.User(3, "bob"),
new UserFollow { FollowerId = 1, FollowedId = 2 });
[Fact]
public async Task Handle_Viewer_MarksFollowedUsers()
{
await SeedAsync(
TestEntities.User(1, "viewer"), TestEntities.User(2, "alice"), TestEntities.User(3, "bob"),
new UserFollow { FollowerId = 1, FollowedId = 2 });
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null, ViewerId: 1), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null, ViewerId: 1), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == 2, item.IsFollowedByViewer));
}
Assert.False(result.IsError);
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == 2, item.IsFollowedByViewer));
}
}
@@ -3,34 +3,33 @@ using Musify.Application.Users;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class IsFollowingUserQueryHandlerTests : HandlerTestBase
{
public sealed class IsFollowingUserQueryHandlerTests : HandlerTestBase
private IsFollowingUserQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_Following_ReturnsTrue()
{
private IsFollowingUserQueryHandler CreateHandler() => new(Database);
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
[Fact]
public async Task Handle_Following_ReturnsTrue()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
Assert.True(result);
}
Assert.True(result);
}
[Fact]
public async Task Handle_NotFollowing_ReturnsFalse()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed);
[Fact]
public async Task Handle_NotFollowing_ReturnsFalse()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed);
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
Assert.False(result);
}
Assert.False(result);
}
}
@@ -2,42 +2,41 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
{
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
private SyncUserCommandHandler CreateHandler() => new(Database, NoOpLogger<SyncUserCommandHandler>());
[Fact]
public async Task Handle_UnknownUser_ProvisionsIt()
{
private SyncUserCommandHandler CreateHandler() => new(Database, NoOpLogger<SyncUserCommandHandler>());
var command = new SyncUserCommand(1, "Jane Doe", "Jane", "Doe", "https://avatar.example/jane.png");
[Fact]
public async Task Handle_UnknownUser_ProvisionsIt()
{
var command = new SyncUserCommand(1, "Jane Doe", "Jane", "Doe", "https://avatar.example/jane.png");
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("Jane Doe", stored.Name);
Assert.Equal("https://avatar.example/jane.png", stored.ProfilePictureUrl);
}
Assert.False(result.IsError);
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("Jane Doe", stored.Name);
Assert.Equal("https://avatar.example/jane.png", stored.ProfilePictureUrl);
}
[Fact]
public async Task Handle_ExistingUser_UpdatesProfileFields()
{
var user = TestEntities.User(1, "Old Name", profilePictureUrl: null);
await SeedAsync(user);
[Fact]
public async Task Handle_ExistingUser_UpdatesProfileFields()
{
var user = TestEntities.User(1, "Old Name", profilePictureUrl: null);
await SeedAsync(user);
var command = new SyncUserCommand(1, "New Name", "New", "Name", "https://avatar.example/new.png");
var command = new SyncUserCommand(1, "New Name", "New", "Name", "https://avatar.example/new.png");
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("New Name", stored.Name);
Assert.Equal("NEW NAME", stored.NormalizedName);
Assert.Equal("https://avatar.example/new.png", stored.ProfilePictureUrl);
}
Assert.False(result.IsError);
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal("New Name", stored.Name);
Assert.Equal("NEW NAME", stored.NormalizedName);
Assert.Equal("https://avatar.example/new.png", stored.ProfilePictureUrl);
}
}
@@ -4,35 +4,34 @@ using Musify.Application.Users;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Users
namespace Musify.Application.Tests.Users;
public sealed class UnfollowUserCommandHandlerTests : HandlerTestBase
{
public sealed class UnfollowUserCommandHandlerTests : HandlerTestBase
private UnfollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<UnfollowUserCommandHandler>());
[Fact]
public async Task Handle_ExistingFollow_RemovesIt()
{
private UnfollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<UnfollowUserCommandHandler>());
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
[Fact]
public async Task Handle_ExistingFollow_RemovesIt()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Empty(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
}
Assert.False(result.IsError);
Assert.Empty(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task Handle_NotFollowing_IsIdempotent()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed);
[Fact]
public async Task Handle_NotFollowing_IsIdempotent()
{
var follower = TestEntities.User(1, "follower");
var followed = TestEntities.User(2, "followed");
await SeedAsync(follower, followed);
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
}
Assert.False(result.IsError);
}
}