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
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
254 lines
9.6 KiB
C#
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);
|
|
}
|