Files
ersatztv/ErsatzTV.Tests/Application/MediaCollections/RerunCollectionHandlerTests.cs
T
timothy ffece01c32
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 13s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m29s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 2m56s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
style: #265 — normalize whitespace in the 11 touched files (format gate #311)
The fix-as-you-touch format gate requires every .cs this PR touches to fully conform to
.editorconfig, including pre-existing whitespace on lines the change didn't edit. dotnet format
(whitespace) applied to the 11 touched files; legacy files left untouched (no big-bang reformat).
Whitespace/layout only — no behavior change. Refs #265
2026-07-12 23:21:00 +02:00

254 lines
9.6 KiB
C#

using ErsatzTV.Application;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
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 Microsoft.EntityFrameworkCore;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Unit = LanguageExt.Unit;
using static LanguageExt.Prelude;
namespace ErsatzTV.Tests.Application.MediaCollections;
[TestFixture]
public class RerunCollectionHandlerTests : MediaCollectionHandlerTestBase
{
[Test]
public async Task Create_Should_Return_Error_When_Collection_Missing()
{
var handler = new CreateRerunCollectionHandler(Db.Factory);
Either<BaseError, RerunCollectionViewModel> result = await handler.Handle(
MakeCreate(CollectionType.Collection, collection: new MediaCollectionViewModel(
CollectionType.Collection,
999,
string.Empty,
false,
MediaItemState.Normal)),
CancellationToken.None);
BaseError error = LeftOf(result);
// deliberate 422-not-404: the missing entity is a request-body FK, not the route resource
error.ShouldNotBeOfType<NotFoundError>();
error.Value.ShouldContain("Collection does not exist");
}
[Test]
public async Task Create_Should_Return_Error_When_CollectionType_Unsupported()
{
var handler = new CreateRerunCollectionHandler(Db.Factory);
// reachable via the legacy Blazor editor (no IsSupportedSelectionType pre-check)
Either<BaseError, RerunCollectionViewModel> result = await handler.Handle(
MakeCreate(CollectionType.Playlist),
CancellationToken.None);
BaseError error = LeftOf(result);
error.Value.ShouldContain("Unsupported collection type");
}
[Test]
public async Task Create_Should_Return_Error_When_MediaItem_Missing()
{
var handler = new CreateRerunCollectionHandler(Db.Factory);
Either<BaseError, RerunCollectionViewModel> result = await handler.Handle(
MakeCreate(CollectionType.Movie, mediaItem: new NamedMediaItemViewModel(999, string.Empty)),
CancellationToken.None);
BaseError error = LeftOf(result);
error.Value.ShouldContain("Movie does not exist");
}
[Test]
public async Task Create_Should_Succeed_When_Collection_Exists()
{
await SeedCollection(1);
var handler = new CreateRerunCollectionHandler(Db.Factory);
Either<BaseError, RerunCollectionViewModel> result = await handler.Handle(
MakeCreate(CollectionType.Collection, collection: new MediaCollectionViewModel(
CollectionType.Collection,
1,
string.Empty,
false,
MediaItemState.Normal)),
CancellationToken.None);
RerunCollectionViewModel vm = RightOf(result);
vm.CollectionType.ShouldBe(CollectionType.Collection);
}
[Test]
public async Task Create_Should_Return_Error_When_MediaItem_Type_Mismatch()
{
await SeedShow(1);
var handler = new CreateRerunCollectionHandler(Db.Factory);
// request claims Movie, but the seeded id (1) is actually a Show
Either<BaseError, RerunCollectionViewModel> result = await handler.Handle(
MakeCreate(CollectionType.Movie, mediaItem: new NamedMediaItemViewModel(1, string.Empty)),
CancellationToken.None);
BaseError error = LeftOf(result);
error.Value.ShouldContain("Movie does not exist");
}
[Test]
public async Task Update_Should_Return_Error_When_SmartCollection_Missing()
{
await SeedRerunCollection(1, CollectionType.SmartCollection);
IMediaCollectionRepository mediaCollectionRepository = Substitute.For<IMediaCollectionRepository>();
mediaCollectionRepository.PlayoutIdsUsingRerunCollection(Arg.Any<int>()).Returns([]);
var handler = new UpdateRerunCollectionHandler(
Db.Factory,
mediaCollectionRepository,
Worker);
Either<BaseError, Unit> result = await handler.Handle(
MakeUpdate(
1,
CollectionType.SmartCollection,
smartCollection: new SmartCollectionViewModel(999, string.Empty, string.Empty)),
CancellationToken.None);
BaseError error = LeftOf(result);
error.Value.ShouldContain("Smart collection does not exist");
}
[Test]
public async Task Update_Should_Succeed_When_Selection_Exists()
{
await SeedRerunCollection(1, CollectionType.SmartCollection);
await SeedSmartCollection(2);
IMediaCollectionRepository mediaCollectionRepository = Substitute.For<IMediaCollectionRepository>();
mediaCollectionRepository.PlayoutIdsUsingRerunCollection(Arg.Any<int>()).Returns([]);
var handler = new UpdateRerunCollectionHandler(
Db.Factory,
mediaCollectionRepository,
Worker);
Either<BaseError, Unit> result = await handler.Handle(
MakeUpdate(
1,
CollectionType.SmartCollection,
smartCollection: new SmartCollectionViewModel(2, string.Empty, string.Empty)),
CancellationToken.None);
RightOf(result);
}
private async Task SeedShow(int id)
{
await using TvContext context = Db.CreateContext();
context.Shows.Add(new Show { Id = id });
await context.SaveChangesAsync();
}
[Test]
public async Task Update_Stale_If_Match_Should_Fail_Precondition()
{
await SeedRerunCollection(1, CollectionType.SmartCollection, version: 2);
await SeedSmartCollection(2);
IMediaCollectionRepository repo = Substitute.For<IMediaCollectionRepository>();
repo.PlayoutIdsUsingRerunCollection(Arg.Any<int>()).Returns([]);
var handler = new UpdateRerunCollectionHandler(Db.Factory, repo, Worker);
Either<BaseError, Unit> result = await handler.Handle(
MakeUpdate(1, CollectionType.SmartCollection, smartCollection: new SmartCollectionViewModel(2, "", ""))
with
{ ExpectedVersions = Some(new[] { 1 }.ToSeq()) },
CancellationToken.None);
LeftOf(result).ShouldBeOfType<PreconditionFailedError>();
(await ReadVersion(1)).ShouldBe(2);
}
[Test]
public async Task Update_Matching_If_Match_Should_Succeed_And_Bump()
{
await SeedRerunCollection(1, CollectionType.SmartCollection, version: 2);
await SeedSmartCollection(2);
IMediaCollectionRepository repo = Substitute.For<IMediaCollectionRepository>();
repo.PlayoutIdsUsingRerunCollection(Arg.Any<int>()).Returns([]);
var handler = new UpdateRerunCollectionHandler(Db.Factory, repo, Worker);
Either<BaseError, Unit> result = await handler.Handle(
MakeUpdate(1, CollectionType.SmartCollection, smartCollection: new SmartCollectionViewModel(2, "", ""))
with
{ ExpectedVersions = Some(new[] { 2 }.ToSeq()) },
CancellationToken.None);
RightOf(result);
(await ReadVersion(1)).ShouldBe(3);
}
private async Task<int> ReadVersion(int id)
{
await using TvContext context = Db.CreateContext();
return await context.RerunCollections.Where(c => c.Id == id).Select(c => c.Version).SingleAsync();
}
private async Task SeedRerunCollection(int id, CollectionType collectionType, string name = "Rerun", int version = 0)
{
await using TvContext context = Db.CreateContext();
context.RerunCollections.Add(new RerunCollection
{
Id = id,
Name = name,
CollectionType = collectionType,
Version = version,
FirstRunPlaybackOrder = PlaybackOrder.Chronological,
RerunPlaybackOrder = PlaybackOrder.Chronological
});
await context.SaveChangesAsync();
}
private static CreateRerunCollection MakeCreate(
CollectionType collectionType,
MediaCollectionViewModel collection = null,
MultiCollectionViewModel multiCollection = null,
SmartCollectionViewModel smartCollection = null,
NamedMediaItemViewModel mediaItem = null) =>
new(
"Rerun " + Guid.NewGuid(),
collectionType,
collection,
multiCollection,
smartCollection,
mediaItem,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological);
private static UpdateRerunCollection MakeUpdate(
int rerunCollectionId,
CollectionType collectionType,
MediaCollectionViewModel collection = null,
MultiCollectionViewModel multiCollection = null,
SmartCollectionViewModel smartCollection = null,
NamedMediaItemViewModel mediaItem = null) =>
new(
rerunCollectionId,
"Rerun " + Guid.NewGuid(),
collectionType,
collection,
multiCollection,
smartCollection,
mediaItem,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological);
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);
}