using ErsatzTV.Application; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.Search; using ErsatzTV.Core; using ErsatzTV.Core.Api.LibraryBrowse; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; using LanguageExt; using ErsatzTV.Tests.Support; using NSubstitute; using NUnit.Framework; using Shouldly; using Unit = LanguageExt.Unit; namespace ErsatzTV.Tests.Application.MediaCollections; [TestFixture] public class CollectionHandlerTests : MediaCollectionHandlerTestBase { [Test] public async Task Update_Should_Return_NotFoundError_When_Collection_Missing() { var handler = new UpdateCollectionHandler( Db.Factory, Substitute.For(), Worker, SearchTargets); Either result = await handler.Handle(new UpdateCollection(999, "Updated"), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task Delete_Should_Return_NotFoundError_When_Collection_Missing() { var handler = new DeleteCollectionHandler(Db.Factory, SearchTargets); Either result = await handler.Handle(new DeleteCollection(999), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task AddItems_Should_Return_NotFoundError_When_Collection_Missing() { IMovieRepository movieRepository = Substitute.For(); movieRepository.AllMoviesExist(Arg.Any>()).Returns(true); ITelevisionRepository televisionRepository = Substitute.For(); televisionRepository.AllShowsExist(Arg.Any>()).Returns(true); televisionRepository.AllSeasonsExist(Arg.Any>()).Returns(true); televisionRepository.AllEpisodesExist(Arg.Any>()).Returns(true); var handler = new AddItemsToCollectionHandler( Db.Factory, Substitute.For(), movieRepository, televisionRepository, Worker, System.Threading.Channels.Channel.CreateUnbounded().Writer); Either result = await handler.Handle(MakeAddItems(collectionId: 999, movieIds: [1]), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task AddItems_Should_Return_ValidationError_When_Generic_MediaItem_Missing() { await SeedCollection(1); IMovieRepository movieRepository = Substitute.For(); movieRepository.AllMoviesExist(Arg.Any>()).Returns(true); ITelevisionRepository televisionRepository = Substitute.For(); televisionRepository.AllShowsExist(Arg.Any>()).Returns(true); televisionRepository.AllSeasonsExist(Arg.Any>()).Returns(true); televisionRepository.AllEpisodesExist(Arg.Any>()).Returns(true); var handler = new AddItemsToCollectionHandler( Db.Factory, Substitute.For(), movieRepository, televisionRepository, Worker, System.Threading.Channels.Channel.CreateUnbounded().Writer); Either result = await handler.Handle( new AddItemsToCollection(1, [], [], [], [], [], [], [], [], [], [999]), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("Media item does not exist"); } [Test] public async Task RemoveItems_Should_Return_NotFoundError_When_Collection_Missing() { var handler = new RemoveItemsFromCollectionHandler( Db.Factory, Substitute.For(), Worker, System.Threading.Channels.Channel.CreateUnbounded().Writer); Either result = await handler.Handle( new RemoveItemsFromCollection(999) { MediaItemIds = [1] }, CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task RemoveItems_Should_Return_NotFoundError_When_Association_Missing() { await SeedCollection(1); var handler = new RemoveItemsFromCollectionHandler( Db.Factory, Substitute.For(), Worker, System.Threading.Channels.Channel.CreateUnbounded().Writer); Either result = await handler.Handle( new RemoveItemsFromCollection(1) { MediaItemIds = [999] }, CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task GetItems_Should_Return_NotFoundError_When_Collection_Missing() { var handler = new GetCollectionItemsHandler(Db.Factory); Either result = await handler.Handle(new GetCollectionItems(999, 0, 100), CancellationToken.None); LeftOf(result).ShouldBeOfType(); } [Test] public async Task GetItems_Should_Return_Members_With_Rooted_Artwork() { await SeedCollectionWithMovie(collectionId: 1, movieId: 10, title: "Fake Movie", poster: "movie.jpg"); var handler = new GetCollectionItemsHandler(Db.Factory); Either result = await handler.Handle(new GetCollectionItems(1, 0, 100), CancellationToken.None); PagedLibraryBrowseItemsResponseModel page = result.Match( Left: _ => throw new AssertionException("Expected a Right result"), Right: value => value); page.TotalCount.ShouldBe(1); page.Page.Count.ShouldBe(1); page.Page[0].Title.ShouldBe("Fake Movie"); page.Page[0].MediaItemId.ShouldBe(10); page.Page[0].MediaType.ShouldBe(LibraryBrowseMediaType.Movie); page.Page[0].Artwork.ShouldBe("/artwork/posters/movie.jpg"); } private async Task SeedCollectionWithMovie(int collectionId, int movieId, string title, string poster) { await using TvContext context = Db.CreateContext(); var library = new LocalLibrary { Id = collectionId, Name = "Local", MediaKind = LibraryMediaKind.Movies, Paths = [] }; var path = new LibraryPath { Id = collectionId, Path = "/media", Library = library, LibraryFolders = [], MediaItems = [] }; library.Paths.Add(path); var collection = new Collection { Id = collectionId, Name = "Collection", MediaItems = [] }; var movie = new Movie { Id = movieId, LibraryPath = path, Collections = [collection], CollectionItems = [], TraktListItems = [], MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(42) }], MovieMetadata = [ new MovieMetadata { Title = title, SortTitle = title, Artwork = [new Artwork { Path = poster, ArtworkKind = ArtworkKind.Poster }], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [], Directors = [], Writers = [] } ] }; collection.MediaItems.Add(movie); context.LocalLibraries.Add(library); context.Collections.Add(collection); context.Movies.Add(movie); await context.SaveChangesAsync(); } private static AddItemsToCollection MakeAddItems(int collectionId, List? movieIds = null) => new( collectionId, movieIds ?? [], [], [], [], [], [], [], [], [], []); private static BaseError LeftOf(Either either) => either.Match(Left: e => e, Right: _ => throw new AssertionException("Expected a Left result")); }