AddItemsToPlaylistHandler only validated existence for movies/shows/ seasons/episodes, leaving artist/music-video/other-video/song/image/ remote-stream ids unchecked (silently accepted, or in RemoteStream's case silently dropped entirely - the apply dictionary never included CollectionType.RemoteStream). Mirror AddItemsToCollectionHandler's established pattern: add RemoteStream to the apply dictionary, and add an aggregate existence check (ValidateMediaItems/GetRequestedMediaItemIds) across all ten kinds against dbContext.MediaItems. Add ErsatzTV.Tests/Application/MediaCollections/PlaylistHandlerTests.cs covering: a bogus id of each of the ten kinds fails validation; a valid RemoteStream id is actually persisted to the playlist (regression test for the drop bug). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
5.4 KiB
C#
140 lines
5.4 KiB
C#
using ErsatzTV.Application.MediaCollections;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Errors;
|
|
using ErsatzTV.Core.Interfaces.Repositories;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Tests.Support;
|
|
using LanguageExt;
|
|
using NSubstitute;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
using Unit = LanguageExt.Unit;
|
|
|
|
namespace ErsatzTV.Tests.Application.MediaCollections;
|
|
|
|
[TestFixture]
|
|
public class PlaylistHandlerTests : MediaCollectionHandlerTestBase
|
|
{
|
|
[TestCase(CollectionType.Artist)]
|
|
[TestCase(CollectionType.MusicVideo)]
|
|
[TestCase(CollectionType.OtherVideo)]
|
|
[TestCase(CollectionType.Song)]
|
|
[TestCase(CollectionType.Image)]
|
|
[TestCase(CollectionType.RemoteStream)]
|
|
public async Task AddItems_Should_Return_ValidationError_When_Previously_Unvalidated_Kind_Missing(
|
|
CollectionType collectionType)
|
|
{
|
|
await SeedPlaylist(1);
|
|
var handler = MakeHandler();
|
|
|
|
Either<BaseError, Unit> result =
|
|
await handler.Handle(MakeAddItems(1, collectionType, [999]), CancellationToken.None);
|
|
|
|
BaseError error = LeftOf(result);
|
|
error.ShouldNotBeOfType<NotFoundError>();
|
|
error.Value.ShouldContain("Media item does not exist");
|
|
}
|
|
|
|
[TestCase(CollectionType.Movie)]
|
|
[TestCase(CollectionType.TelevisionShow)]
|
|
[TestCase(CollectionType.TelevisionSeason)]
|
|
[TestCase(CollectionType.Episode)]
|
|
public async Task AddItems_Should_Return_ValidationError_When_Existing_Validated_Kind_Missing(
|
|
CollectionType collectionType)
|
|
{
|
|
await SeedPlaylist(1);
|
|
var handler = MakeHandler();
|
|
|
|
Either<BaseError, Unit> result =
|
|
await handler.Handle(MakeAddItems(1, collectionType, [999]), CancellationToken.None);
|
|
|
|
BaseError error = LeftOf(result);
|
|
error.ShouldNotBeOfType<NotFoundError>();
|
|
}
|
|
|
|
[Test]
|
|
public async Task AddItems_Should_Add_Valid_RemoteStream_Item_To_Playlist()
|
|
{
|
|
await SeedPlaylist(1);
|
|
await SeedRemoteStream(50);
|
|
var handler = MakeHandler();
|
|
|
|
Either<BaseError, Unit> result =
|
|
await handler.Handle(MakeAddItems(1, CollectionType.RemoteStream, [50]), CancellationToken.None);
|
|
|
|
RightOf(result);
|
|
|
|
await using TvContext context = Db.CreateContext();
|
|
Playlist playlist = await context.Playlists.FindAsync(1);
|
|
List<PlaylistItem> items = context.Entry(playlist!).Collection(p => p.Items).Query().ToList();
|
|
|
|
items.ShouldHaveSingleItem();
|
|
items[0].CollectionType.ShouldBe(CollectionType.RemoteStream);
|
|
items[0].MediaItemId.ShouldBe(50);
|
|
}
|
|
|
|
private AddItemsToPlaylistHandler MakeHandler()
|
|
{
|
|
IMovieRepository movieRepository = Substitute.For<IMovieRepository>();
|
|
movieRepository.AllMoviesExist(Arg.Any<List<int>>()).Returns(true);
|
|
ITelevisionRepository televisionRepository = Substitute.For<ITelevisionRepository>();
|
|
televisionRepository.AllShowsExist(Arg.Any<List<int>>()).Returns(true);
|
|
televisionRepository.AllSeasonsExist(Arg.Any<List<int>>()).Returns(true);
|
|
televisionRepository.AllEpisodesExist(Arg.Any<List<int>>()).Returns(true);
|
|
|
|
return new AddItemsToPlaylistHandler(Db.Factory, movieRepository, televisionRepository);
|
|
}
|
|
|
|
private async Task SeedPlaylist(int id, string name = "Playlist")
|
|
{
|
|
await using TvContext context = Db.CreateContext();
|
|
context.Playlists.Add(new Playlist { Id = id, Name = name, Items = [] });
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task SeedRemoteStream(int id)
|
|
{
|
|
await using TvContext context = Db.CreateContext();
|
|
context.RemoteStreams.Add(new RemoteStream
|
|
{
|
|
Id = id,
|
|
RemoteStreamMetadata = []
|
|
});
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
private static AddItemsToPlaylist MakeAddItems(int playlistId, CollectionType collectionType, List<int> ids)
|
|
{
|
|
List<int> movieIds = collectionType == CollectionType.Movie ? ids : [];
|
|
List<int> showIds = collectionType == CollectionType.TelevisionShow ? ids : [];
|
|
List<int> seasonIds = collectionType == CollectionType.TelevisionSeason ? ids : [];
|
|
List<int> episodeIds = collectionType == CollectionType.Episode ? ids : [];
|
|
List<int> artistIds = collectionType == CollectionType.Artist ? ids : [];
|
|
List<int> musicVideoIds = collectionType == CollectionType.MusicVideo ? ids : [];
|
|
List<int> otherVideoIds = collectionType == CollectionType.OtherVideo ? ids : [];
|
|
List<int> songIds = collectionType == CollectionType.Song ? ids : [];
|
|
List<int> imageIds = collectionType == CollectionType.Image ? ids : [];
|
|
List<int> remoteStreamIds = collectionType == CollectionType.RemoteStream ? ids : [];
|
|
|
|
return new AddItemsToPlaylist(
|
|
playlistId,
|
|
movieIds,
|
|
showIds,
|
|
seasonIds,
|
|
episodeIds,
|
|
artistIds,
|
|
musicVideoIds,
|
|
otherVideoIds,
|
|
songIds,
|
|
imageIds,
|
|
remoteStreamIds);
|
|
}
|
|
|
|
private static BaseError LeftOf<TR>(Either<BaseError, TR> either) =>
|
|
either.Match(Left: e => e, Right: _ => throw new AssertionException("Expected a Left result"));
|
|
|
|
private static TR RightOf<TR>(Either<BaseError, TR> either) =>
|
|
either.Match(Left: e => throw new AssertionException($"Expected a Right result, got: {e.Value}"), Right: v => v);
|
|
}
|