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
@@ -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));
}
}