Files
ersatztv/ErsatzTV.Tests/Application/MediaCollections/PlaylistHandlerTests.cs
T
timothyandClaude Fable 5 fc2c054b44 fix(api): validate + include RemoteStream in playlist add-items (fixes #217)
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>
2026-07-10 23:27:57 +02:00

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