Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
@@ -8,125 +8,126 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private CreateTrackCommandHandler CreateHandler()
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
return new CreateTrackCommandHandler(
Database,
eventBus,
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
NoOpLogger<CreateTrackCommandHandler>(),
TestConfigurations.Track());
}
private CreateTrackCommandHandler CreateHandler()
{
storageService
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(new ObjectMetaData("image/webp", 1024));
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"));
return new CreateTrackCommandHandler(
Database,
eventBus,
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
NoOpLogger<CreateTrackCommandHandler>(),
TestConfigurations.Track());
}
[Fact]
public async Task Handle_SameIntentForPictureAndAudio_ReturnsValidationError()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
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"));
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, picture.Id, [Genre.Pop]);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_SameIntentForPictureAndAudio_ReturnsValidationError()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, picture.Id, [Genre.Pop]);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
}
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop, Genre.Rock]);
[Fact]
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
{
var user = TestEntities.User();
var (picture, audio) = SeedIntents(user.Id);
await SeedAsync(user, picture, audio);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop, Genre.Rock]);
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 result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
}
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>());
[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 stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored);
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
}
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit);
[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 result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], 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);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop]);
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);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_UserMissing_ReturnsNotFound()
{
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop]);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
[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);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
[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 result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
Assert.True(result.IsError);
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
}
var result = await CreateHandler().Handle(command, TestContext.Current.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);
Assert.True(result.IsError);
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
}
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
[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 result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -3,29 +3,30 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
[Fact]
public async Task Handle_DeletesOnlyOldUncountedListens()
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
{
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);
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
var deleted = await CreateHandler().Handle(new DeleteStaleUncountedListensCommand(), TestContext.Current.CancellationToken);
[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);
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);
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);
}
}
}
@@ -6,60 +6,61 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
[Fact]
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
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>());
}
[Fact]
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
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>());
}
[Fact]
public async Task Handle_NotOwner_ReturnsForbidden()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, track);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new DeleteTrackCommand(stranger.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
[Fact]
public async Task Handle_NotOwner_ReturnsForbidden()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var track = TestEntities.Track(owner);
await SeedAsync(owner, stranger, track);
[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(stranger.Id, track.Id), TestContext.Current.CancellationToken);
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
[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);
}
}
}
@@ -2,37 +2,38 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetLastTrackListenedByUserIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private GetLastTrackListenedByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_MultipleListens_ReturnsMostRecentlyListenedTrack()
public sealed class GetLastTrackListenedByUserIdQueryHandlerTests : HandlerTestBase
{
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));
private GetLastTrackListenedByUserIdQueryHandler CreateHandler() => new(Database);
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
[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));
Assert.NotNull(result);
Assert.Equal("Newer listen", result.Title);
}
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoHistory_ReturnsNull()
{
var owner = TestEntities.User();
await SeedAsync(owner);
Assert.NotNull(result);
Assert.Equal("Newer listen", result.Title);
}
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoHistory_ReturnsNull()
{
var owner = TestEntities.User();
await SeedAsync(owner);
Assert.Null(result);
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.Null(result);
}
}
}
@@ -2,53 +2,54 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_MultipleListens_ReturnsDistinctTracksMostRecentFirst()
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
{
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));
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
[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));
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
}
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoHistory_ReturnsEmpty()
{
var owner = TestEntities.User();
await SeedAsync(owner);
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
}
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_NoHistory_ReturnsEmpty()
{
var owner = TestEntities.User();
await SeedAsync(owner);
Assert.Empty(result);
}
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
[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));
Assert.Empty(result);
}
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
[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));
Assert.Equal(["Counted"], result.Select(track => track.Title));
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
Assert.Equal(["Counted"], result.Select(track => track.Title));
}
}
}
@@ -4,49 +4,50 @@ using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_LocalTrack_ReturnsItWithOwnerAsArtist()
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
{
var owner = TestEntities.User(1, "artist-name");
var track = TestEntities.Track(owner, "My Song");
await SeedAsync(owner, track);
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
[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);
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);
}
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), 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.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
[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));
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
[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));
Assert.Equal(2, result.Value.ListensCount);
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
Assert.Equal(2, result.Value.ListensCount);
}
}
}
@@ -2,26 +2,27 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_ReturnsOnlyTracksLinkedToThatUser()
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
{
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);
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
[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);
Assert.False(result.IsError);
var track = Assert.Single(result.Value.Items);
Assert.Equal("Mine", track.Title);
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);
}
}
}
@@ -3,51 +3,52 @@ using Musify.Application.Tracks;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private GetTracksQueryHandler CreateHandler() => new(Database);
[Fact]
public async Task Handle_NoNameFilter_ReturnsEveryTrackAUserOwns()
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
{
var owner = TestEntities.User();
var trackA = TestEntities.Track(owner, "Alpha");
var trackB = TestEntities.Track(owner, "Beta");
await SeedAsync(
owner, trackA, trackB);
private GetTracksQueryHandler CreateHandler() => new(Database);
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.Equal(2, result.TotalItemCount);
Assert.Equal(2, result.Items.Count);
}
[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);
[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);
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
Assert.Equal(2, result.TotalItemCount);
Assert.Equal(2, result.Items.Count);
}
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var item = Assert.Single(result.Items);
Assert.Equal("Bohemian Rhapsody", item.Title);
}
[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);
[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);
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
var item = Assert.Single(result.Items);
Assert.Equal("Bohemian Rhapsody", item.Title);
}
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
var item = Assert.Single(result.Items);
Assert.Equal("Loud", item.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);
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
var item = Assert.Single(result.Items);
Assert.Equal("Loud", item.Title);
}
}
}
@@ -5,109 +5,110 @@ using Musify.Application.Tracks;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
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)
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
{
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 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)
{
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);
}
[Fact]
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var stored = await ReloadAsync(listen.Id);
Assert.Equal(10, stored.PlayedSeconds);
Assert.False(stored.IsCounted);
}
[Fact]
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
[Fact]
public async Task Handle_ShortTrack_CountsAtHalfDuration()
{
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
[Fact]
public async Task Handle_LowerValueThanStored_KeepsHighest()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var handler = CreateHandler();
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);
var stored = await ReloadAsync(listen.Id);
Assert.Equal(40, stored.PlayedSeconds);
Assert.True(stored.IsCounted);
}
[Fact]
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
var stored = await ReloadAsync(listen.Id);
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
}
[Fact]
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
{
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
}
[Fact]
public async Task Handle_OtherUsersListen_ReturnsNotFound()
{
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
Assert.True(result.IsError);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
[Fact]
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var stored = await ReloadAsync(listen.Id);
Assert.Equal(10, stored.PlayedSeconds);
Assert.False(stored.IsCounted);
}
[Fact]
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
[Fact]
public async Task Handle_ShortTrack_CountsAtHalfDuration()
{
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
}
[Fact]
public async Task Handle_LowerValueThanStored_KeepsHighest()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var handler = CreateHandler();
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);
var stored = await ReloadAsync(listen.Id);
Assert.Equal(40, stored.PlayedSeconds);
Assert.True(stored.IsCounted);
}
[Fact]
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
{
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
var stored = await ReloadAsync(listen.Id);
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
}
[Fact]
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
{
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
await CreateHandler().Handle(
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
}
[Fact]
public async Task Handle_OtherUsersListen_ReturnsNotFound()
{
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
var result = await CreateHandler().Handle(
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
Assert.True(result.IsError);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
@@ -9,77 +9,78 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private RequestTrackUploadUrlsCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
{
storageService
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
.Returns("https://storage.musify.test/presigned-upload");
private readonly IStorageService storageService = Substitute.For<IStorageService>();
var config = uploadIntentConfig ?? TestConfigurations.UploadIntent();
private RequestTrackUploadUrlsCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
{
storageService
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
.Returns("https://storage.musify.test/presigned-upload");
return new RequestTrackUploadUrlsCommandHandler(
new UploadIntentIssuer(
Database,
storageService,
new UploadIntentValidator(Database, storageService, config),
var config = uploadIntentConfig ?? TestConfigurations.UploadIntent();
return new RequestTrackUploadUrlsCommandHandler(
new UploadIntentIssuer(
Database,
storageService,
new UploadIntentValidator(Database, storageService, config),
TestConfigurations.Storage(),
config,
NoOpLogger<UploadIntentIssuer>()),
TestConfigurations.Storage(),
config,
NoOpLogger<UploadIntentIssuer>()),
TestConfigurations.Storage(),
TestConfigurations.Track(),
config);
}
TestConfigurations.Track(),
config);
}
[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));
}
}
}
@@ -8,94 +8,95 @@ using Musify.Application.Tracks;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class StartListeningCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
private StartListeningCommandHandler CreateHandler(PlaybackConfiguration? playback = null)
public sealed class StartListeningCommandHandlerTests : HandlerTestBase
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("ticket-token", 60));
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
var issuer = new TrackStreamIssuer(
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test"),
playback ?? TestConfigurations.Playback());
private StartListeningCommandHandler CreateHandler(PlaybackConfiguration? playback = null)
{
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("ticket-token", 60));
return new StartListeningCommandHandler(Database, issuer, playback ?? TestConfigurations.Playback(), NoOpLogger<StartListeningCommandHandler>());
}
var issuer = new TrackStreamIssuer(
ticketService,
TestConfigurations.Track(),
TestConfigurations.StreamGateway("https://stream.musify.test"),
playback ?? TestConfigurations.Playback());
[Fact]
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
return new StartListeningCommandHandler(Database, issuer, playback ?? TestConfigurations.Playback(), NoOpLogger<StartListeningCommandHandler>());
}
var result = await CreateHandler().Handle(new StartListeningCommand(track.Id, owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
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 result = await CreateHandler().Handle(new StartListeningCommand(track.Id, owner.Id), TestContext.Current.CancellationToken);
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);
}
Assert.False(result.IsError);
Assert.Equal("ticket-token", result.Value.Ticket);
Assert.Equal(60, result.Value.ExpiresInSeconds);
Assert.Contains("audio-folder", result.Value.ManifestUrl);
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new StartListeningCommand(Guid.NewGuid(), 1), TestContext.Current.CancellationToken);
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);
}
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackMissing_ReturnsNotFound()
{
var result = await CreateHandler().Handle(new StartListeningCommand(Guid.NewGuid(), 1), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
await SeedAsync(owner, track);
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new StartListeningCommand(track.Id, owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
await SeedAsync(owner, track);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
var result = await CreateHandler().Handle(new StartListeningCommand(track.Id, owner.Id), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningDisabled_ReturnsUnauthorized()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: false));
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningDisabled_ReturnsUnauthorized()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var result = await handler.Handle(new StartListeningCommand(track.Id, UserId: null), TestContext.Current.CancellationToken);
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: false));
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
var result = await handler.Handle(new StartListeningCommand(track.Id, UserId: null), TestContext.Current.CancellationToken);
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningEnabled_IssuesTicketWithoutRecordingHistory()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: true));
[Fact]
public async Task Handle_AnonymousUser_AnonymousListeningEnabled_IssuesTicketWithoutRecordingHistory()
{
var owner = TestEntities.User();
var track = TestEntities.Track(owner);
await SeedAsync(owner, track);
var result = await handler.Handle(new StartListeningCommand(track.Id, UserId: null), TestContext.Current.CancellationToken);
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: true));
Assert.False(result.IsError);
Assert.Equal("ticket-token", result.Value.Ticket);
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
var result = await handler.Handle(new StartListeningCommand(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));
}
}
}