Files
ersatztv/ErsatzTV.Tests/Application/MediaCollections/GetCollectionItemsHandlerTests.cs
T
timothyandClaude Fable 5 f1b3879d51 feat(api): playout erase/scheduling-context + collection custom-order (#210, #211)
Backend slice for the ChicoryTV playouts and collections screens.

PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
  Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
  Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
  playout item's stored context by row id via a new
  GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)

CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
  UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
  when the collection's UseCustomPlaybackOrder is set

Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:05:51 +02:00

139 lines
4.5 KiB
C#

using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Application.MediaCollections;
[TestFixture]
public class GetCollectionItemsHandlerTests
{
private InMemoryTvContext _db = null!;
[SetUp]
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
[TearDown]
public async Task TearDown() => await _db.DisposeAsync();
[Test]
public async Task Handle_Should_Return_NotFound_For_Missing_Collection()
{
var handler = new GetCollectionItemsHandler(_db.Factory);
Either<BaseError, PagedLibraryBrowseItemsResponseModel> result =
await handler.Handle(new GetCollectionItems(999, 0, 100), CancellationToken.None);
result.IsLeft.ShouldBeTrue();
}
[Test]
public async Task Handle_Should_Order_By_Title_When_Custom_Order_Disabled()
{
await SeedCollectionGraph(useCustomPlaybackOrder: false);
var handler = new GetCollectionItemsHandler(_db.Factory);
Either<BaseError, PagedLibraryBrowseItemsResponseModel> result =
await handler.Handle(new GetCollectionItems(10, 0, 100), CancellationToken.None);
PagedLibraryBrowseItemsResponseModel page = result.RightToSeq().Single();
page.TotalCount.ShouldBe(3);
page.Page.Select(i => i.Title).ShouldBe(["Alpha", "Beta", "Zeta"]);
}
[Test]
public async Task Handle_Should_Order_By_CustomIndex_With_Nulls_Last_When_Custom_Order_Enabled()
{
await SeedCollectionGraph(useCustomPlaybackOrder: true);
var handler = new GetCollectionItemsHandler(_db.Factory);
Either<BaseError, PagedLibraryBrowseItemsResponseModel> result =
await handler.Handle(new GetCollectionItems(10, 0, 100), CancellationToken.None);
PagedLibraryBrowseItemsResponseModel page = result.RightToSeq().Single();
// Zeta has CustomIndex 0, Alpha has CustomIndex 1, Beta has no CustomIndex (sorts last).
page.Page.Select(i => i.Title).ShouldBe(["Zeta", "Alpha", "Beta"]);
}
private async Task SeedCollectionGraph(bool useCustomPlaybackOrder)
{
await using TvContext context = _db.CreateContext();
var library = new LocalLibrary
{
Id = 1,
Name = "Library",
MediaKind = LibraryMediaKind.Movies,
Paths = []
};
var path = new LibraryPath
{
Id = 1,
Path = "/media",
Library = library,
LibraryFolders = [],
MediaItems = []
};
library.Paths.Add(path);
Movie alpha = MakeMovie(101, path, "Alpha");
Movie beta = MakeMovie(102, path, "Beta");
Movie zeta = MakeMovie(103, path, "Zeta");
var collection = new Collection
{
Id = 10,
Name = "Manual",
UseCustomPlaybackOrder = useCustomPlaybackOrder,
MediaItems = [],
CollectionItems =
[
new CollectionItem { MediaItemId = 103, CustomIndex = 0 },
new CollectionItem { MediaItemId = 101, CustomIndex = 1 },
new CollectionItem { MediaItemId = 102, CustomIndex = null }
],
MultiCollections = [],
MultiCollectionItems = []
};
context.LocalLibraries.Add(library);
context.Movies.AddRange(alpha, beta, zeta);
context.Collections.Add(collection);
await context.SaveChangesAsync();
}
private static Movie MakeMovie(int id, LibraryPath path, string title) =>
new()
{
Id = id,
LibraryPath = path,
Collections = [],
CollectionItems = [],
TraktListItems = [],
MovieMetadata =
[
new MovieMetadata
{
Title = title,
SortTitle = title,
Artwork = [],
Genres = [],
Tags = [],
Studios = [],
Actors = [],
Guids = [],
Subtitles = [],
Directors = [],
Writers = []
}
],
MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(30) }]
};
}