Add backend test suite under backend/Tests/
Two xUnit projects, wired into Musify.slnx under a /Tests/ folder that
mirrors Core/ and Hosts/:
Musify.Domain.Tests Value-object logic (TrackAudio/TrackPictures'
IsProcessed/IsFailed/IsInProgress). Entities are
otherwise anemic EF models with no behavior to
test.
Musify.Application.Tests All ~45 Application handlers and services
(Albums, Tracks, PlayLists, Users, Mixes,
YouTube, plus TrackStreamIssuer,
UploadIntentValidator, YouTubeTrackProvisioner).
IDatabase exposes DbSet<T> directly, so handlers compose real LINQ queries
against it — that can't be expressed with a mock (no IQueryable provider
behind one). TestSupport/TestDatabase.cs is instead a real DbContext on a
private SQLite in-memory connection per test (fresh per test since xUnit
instantiates the test class per test method): fast, no external dependency,
and — unlike EF Core's InMemory provider — SQLite enforces real foreign keys
and supports transactions, which two handlers need. External dependencies
(IStorageService, IEventBus, IYouTubeMusicService, ...) are substituted with
NSubstitute.
TestSupport/TestEntities.cs and TestConfigurations.cs build fully-populated
domain entities and configuration POCOs with sensible defaults for every
required member, so no test method needs to fill in properties it doesn't
care about, and no builder ever hands back null.
Also, needed to make the Application layer itself testable without a real
Postgres or user-secrets:
- DatabaseDesignTimeFactory now falls back to the Database__ConnectionString
env var, and DesignSettings.json is optional — unblocks the `migrate`
one-shot container from deploy/compose.yml, which hit the same gap.
- Directory.Packages.props: xunit, xunit.runner.visualstudio, NSubstitute,
Microsoft.EntityFrameworkCore.Sqlite (pinned to the same 10.0.8 as the rest
of EF Core), Microsoft.NET.Test.Sdk, coverlet.collector. Also pins
SQLitePCLRaw.bundle_e_sqlite3 to 2.1.13 — EFCore.Sqlite 10.0.8 pulls 2.1.11
transitively, which has a known high-severity advisory (GHSA-2m69-gcr7-jv3q).
- backend/.dockerignore excludes Tests/: no application Dockerfile needs it
in its build context.
162 tests, all green: `dotnet test backend/Musify.slnx`.
This commit is contained in:
+92
@@ -0,0 +1,92 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private AddTrackToPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToPlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FirstTrack_AddsItAtPositionZero()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, playList, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var link = Assert.Single(await Database.PlayListHasTracks.ToListAsync());
|
||||
Assert.Equal(0, link.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListAlreadyHasTracks_AppendsAtNextPosition()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var existingTrack = TestEntities.LocalTrack(owner, "Existing");
|
||||
var newTrack = TestEntities.LocalTrack(owner, "New");
|
||||
await SeedAsync(
|
||||
owner, playList, existingTrack, newTrack,
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = existingTrack.Id, Position = 0 });
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, newTrack.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var newLink = await Database.PlayListHasTracks.SingleAsync(link => link.TrackId == newTrack.Id);
|
||||
Assert.Equal(1, newLink.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyInPlayList_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class AddYouTubeTrackToPlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
|
||||
|
||||
private AddYouTubeTrackToPlayListCommandHandler CreateHandler()
|
||||
{
|
||||
var provisioner = new YouTubeTrackProvisioner(
|
||||
Database, youTubeMusicService, TestConfigurations.Track(), NoOpLogger<YouTubeTrackProvisioner>());
|
||||
|
||||
return new AddYouTubeTrackToPlayListCommandHandler(
|
||||
Database,
|
||||
eventBus,
|
||||
youTubeMusicService,
|
||||
provisioner,
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
NoOpLogger<AddYouTubeTrackToPlayListCommandHandler>());
|
||||
}
|
||||
|
||||
private static YouTubeSongResult BuildSong(string videoId) => new(
|
||||
videoId, "Song Title", "Song Artist", "Song Album", 200, "https://img.example/thumb.jpg", false,
|
||||
[new YouTubeArtistRef("artist-id", "Song Artist")]);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyProcessedAndNotLinked_AddsItWithoutQueuingDownload()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.ExternalTrack(externalId: "abc123");
|
||||
await SeedAsync(owner, playList, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddYouTubeTrackToPlayListCommand(owner.Id, playList.Id, "abc123"), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(await Database.PlayListHasTracks.ToListAsync());
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.DownloadYouTubeTrackEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UnknownVideo_ProvisionsTrackAndQueuesDownload()
|
||||
{
|
||||
youTubeMusicService.GetSongAsync("new-video", Arg.Any<CancellationToken>()).Returns(BuildSong("new-video"));
|
||||
youTubeMusicService.ResolveArtworkUrl(Arg.Any<string>()).Returns(callInfo => callInfo.Arg<string>());
|
||||
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddYouTubeTrackToPlayListCommand(owner.Id, playList.Id, "new-video"), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Song Title", result.Value.Title);
|
||||
var track = await Database.ExternalTracks.SingleAsync(t => t.ExternalId == "new-video");
|
||||
Assert.True(track.Audio.DownloadRequested);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DownloadYouTubeTrackEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new AddYouTubeTrackToPlayListCommand(1, Guid.NewGuid(), "abc123"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddYouTubeTrackToPlayListCommand(stranger.Id, playList.Id, "abc123"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyLinked_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.ExternalTrack(externalId: "abc123");
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
var result = await CreateHandler().Handle(new AddYouTubeTrackToPlayListCommand(owner.Id, playList.Id, "abc123"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private CreatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreatePlayListCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
TestConfigurations.UploadIntent());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoPictureIntent_CreatesPlayListWithPresetPictures()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", "A description", PictureIntentId: null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Playlist", result.Value.Name);
|
||||
Assert.Equal("PresetSmallPicture.webp", result.Value.SmallImageKeyName);
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidPictureIntent_PublishesResourcesEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
var user = TestEntities.User();
|
||||
var intent = TestEntities.UploadIntent(user.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "cover.webp");
|
||||
await SeedAsync(user, intent);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, intent.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.PlayLists.FindAsync(result.Value.Id);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("cover.webp", stored.Pictures.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreatePlayListCommand(404, "Orphan Playlist", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentNotFound_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, Guid.NewGuid());
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
|
||||
private DeletePlayListCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeletePlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_MarksPlayListAsRemoving()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(owner.Id, playList.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.PlayLists.FindAsync(playList.Id);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeletePlayListEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(1, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(stranger.Id, playList.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetPlayListByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(playList.Name, result.Value.Name);
|
||||
Assert.Equal([track.Id], result.Value.CoverTrackIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetPlayListCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.PlayList());
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
public async Task Handle_ExistingPlayList_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, size), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(Guid.NewGuid(), "small"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetPlayListTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TracksOrderedByPosition_ReturnsThemInOrder()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var second = TestEntities.LocalTrack(owner, "Second");
|
||||
var first = TestEntities.LocalTrack(owner, "First");
|
||||
await SeedAsync(
|
||||
owner, playList, second, first,
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = second.Id, Position = 1 },
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = first.Id, Position = 0 });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.Id, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetPlayListsByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyActivePlayListsForThatUser()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
await SeedAsync(
|
||||
owner, other,
|
||||
TestEntities.PlayList(owner.Id, "Mine"),
|
||||
TestEntities.PlayList(owner.Id, "Being removed", lifeCycleStatus: LifeCycleStatus.Removing),
|
||||
TestEntities.PlayList(other.Id, "Theirs"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", playList.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NameFilter_ReturnsOnlyMatchingPlayLists()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "Road Trip"), TestEntities.PlayList(owner.Id, "Study Focus"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: "road", PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Road Trip", playList.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetPlayListsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryPlayListPaged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "One"), TestEntities.PlayList(owner.Id, "Two"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoPlayLists_ReturnsEmptyPage()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private RemoveTrackFromPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<RemoveTrackFromPlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInPlayList_RemovesTheLink()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(Database.PlayListHasTracks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotLinked_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestPlayListPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
|
||||
return new RequestPlayListPictureUploadCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestPlayListPictureUploadCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var command = new RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
|
||||
Assert.Single(await Database.UploadIntents.ToListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestPlayListPictureUploadCommand(404, "webp", "image/webp");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadBytesPerUser = 100;
|
||||
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var command = new RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
|
||||
|
||||
var result = await CreateHandler(config).Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Empty(await Database.UploadIntents.ToListAsync());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private UpdatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<UpdatePlayListCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
TestConfigurations.UploadIntent());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RenamesPlayList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Old Name");
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("New Name", result.Value.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " ", " ", null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Original Name", result.Value.Name);
|
||||
Assert.Equal("Original description", result.Value.Description);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user