using ErsatzTV.Application; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.Search; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Tests.Support; using LanguageExt; using Microsoft.EntityFrameworkCore; using NSubstitute; using NUnit.Framework; using Shouldly; using Unit = LanguageExt.Unit; namespace ErsatzTV.Tests.Integration; /// /// End-to-end create -> read -> add item -> remove item -> delete against the in-memory SQLite harness, /// exercising the real EF Core handlers and collection item persistence. /// [TestFixture] public class CollectionLifecycleIntegrationTests : MediaCollectionHandlerTestBase { [Test] public async Task Create_Read_AddItem_RemoveItem_Delete() { int movieId = await SeedMovie(); IMediaCollectionRepository mediaCollectionRepository = Substitute.For(); mediaCollectionRepository.PlayoutIdsUsingCollection(Arg.Any()).Returns([]); 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 createHandler = new CreateCollectionHandler(Db.Factory, SearchTargets); Either created = await createHandler.Handle(new CreateCollection("Integration"), CancellationToken.None); int collectionId = created.Match(Left: _ => throw new AssertionException("create failed"), Right: r => r.Id); collectionId.ShouldBeGreaterThan(0); var getHandler = new GetCollectionByIdHandler(Db.Factory); Option afterCreate = await getHandler.Handle(new GetCollectionById(collectionId), CancellationToken.None); afterCreate.IsSome.ShouldBeTrue(); afterCreate.Match( Some: vm => vm.Name.ShouldBe("Integration"), None: () => throw new AssertionException("expected collection to exist")); var addHandler = new AddItemsToCollectionHandler( Db.Factory, mediaCollectionRepository, movieRepository, televisionRepository, Worker, System.Threading.Channels.Channel.CreateUnbounded().Writer); Either added = await addHandler.Handle(MakeAddItems(collectionId, movieId), CancellationToken.None); added.IsRight.ShouldBeTrue(); await using (TvContext context = Db.CreateContext()) { bool itemExists = await context.CollectionItems .AnyAsync(ci => ci.CollectionId == collectionId && ci.MediaItemId == movieId); itemExists.ShouldBeTrue(); } var removeHandler = new RemoveItemsFromCollectionHandler( Db.Factory, mediaCollectionRepository, Worker, System.Threading.Channels.Channel.CreateUnbounded().Writer); Either removed = await removeHandler.Handle( new RemoveItemsFromCollection(collectionId) { MediaItemIds = [movieId] }, CancellationToken.None); removed.IsRight.ShouldBeTrue(); await using (TvContext context = Db.CreateContext()) { bool itemExists = await context.CollectionItems .AnyAsync(ci => ci.CollectionId == collectionId && ci.MediaItemId == movieId); itemExists.ShouldBeFalse(); } var deleteHandler = new DeleteCollectionHandler(Db.Factory, SearchTargets); Either deleted = await deleteHandler.Handle(new DeleteCollection(collectionId), CancellationToken.None); deleted.IsRight.ShouldBeTrue(); Option afterDelete = await getHandler.Handle(new GetCollectionById(collectionId), CancellationToken.None); afterDelete.IsNone.ShouldBeTrue(); } private async Task SeedMovie() { await using TvContext context = Db.CreateContext(); var mediaSource = new LocalMediaSource(); var library = new LocalLibrary { Name = "Movies", MediaKind = LibraryMediaKind.Movies, MediaSource = mediaSource, Paths = [] }; var libraryPath = new LibraryPath { Path = "/media/movies", Library = library, LibraryFolders = [], MediaItems = [] }; var movie = new Movie { LibraryPath = libraryPath, MovieMetadata = [], MediaVersions = [], Collections = [] }; context.Movies.Add(movie); await context.SaveChangesAsync(); return movie.Id; } private static AddItemsToCollection MakeAddItems(int collectionId, int movieId) => new(collectionId, [movieId], [], [], [], [], [], [], [], [], []); }