diff --git a/ErsatzTV.Tests/Controllers/RerunCollectionControllerTests.cs b/ErsatzTV.Tests/Controllers/RerunCollectionControllerTests.cs index 3da210db6..9a6430775 100644 --- a/ErsatzTV.Tests/Controllers/RerunCollectionControllerTests.cs +++ b/ErsatzTV.Tests/Controllers/RerunCollectionControllerTests.cs @@ -155,6 +155,22 @@ public class RerunCollectionControllerTests result.ShouldBeOfType(); } + [Test] + public async Task Create_Should_Return_422_For_Unsupported_Collection_Type() + { + IActionResult result = await _controller.Create( + new CreateRerunCollectionRequest( + "Nightly", + CollectionType.Playlist, + 1, + PlaybackOrder.Chronological, + PlaybackOrder.Chronological), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + [Test] public async Task Update_Should_Return_200_With_Refreshed_Body_And_Resolve_Selection() { @@ -206,6 +222,23 @@ public class RerunCollectionControllerTests await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } + [Test] + public async Task Update_Should_Return_422_For_Unsupported_Collection_Type() + { + IActionResult result = await _controller.Update( + 4, + new UpdateRerunCollectionRequest( + "Nightly", + CollectionType.RerunFirstRun, + 1, + PlaybackOrder.Chronological, + PlaybackOrder.Chronological), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + [Test] public async Task Delete_Should_Return_204_On_Success() { diff --git a/ErsatzTV/Controllers/Api/Requests/RerunCollectionRequestMapping.cs b/ErsatzTV/Controllers/Api/Requests/RerunCollectionRequestMapping.cs index c5b215aae..61899bca8 100644 --- a/ErsatzTV/Controllers/Api/Requests/RerunCollectionRequestMapping.cs +++ b/ErsatzTV/Controllers/Api/Requests/RerunCollectionRequestMapping.cs @@ -14,6 +14,27 @@ namespace ErsatzTV.Controllers.Api.Requests; // doing extra lookup queries. The remaining VMs are left null (the handler expects that). internal static class RerunCollectionRequestMapping { + /// + /// The only values valid as a rerun collection's selected + /// source. Excludes Playlist / RerunFirstRun / RerunRerun / SearchQuery / Fake* — those + /// are not media-item-backed collection types and must not be resolved via + /// . Callers (the controller) must check this first. + /// + public static bool IsSupportedSelectionType(CollectionType collectionType) => + collectionType is CollectionType.Collection + or CollectionType.MultiCollection + or CollectionType.SmartCollection + or CollectionType.TelevisionShow + or CollectionType.TelevisionSeason + or CollectionType.Artist + or CollectionType.Movie + or CollectionType.Episode + or CollectionType.MusicVideo + or CollectionType.OtherVideo + or CollectionType.Song + or CollectionType.Image + or CollectionType.RemoteStream; + public static (MediaCollectionViewModel Collection, MultiCollectionViewModel MultiCollection, SmartCollectionViewModel SmartCollection, @@ -43,7 +64,9 @@ internal static class RerunCollectionRequestMapping // Any media-item-backed collection type (TelevisionShow, TelevisionSeason, // Artist, Movie, Episode, MusicVideo, OtherVideo, Song, Image, ...) resolves to - // the MediaItem VM keyed by media item id. + // the MediaItem VM keyed by media item id. The caller (RerunCollectionController) + // has already validated CollectionType via IsSupportedSelectionType, so this + // catch-all only ever receives a real media-item type. _ => ( null, null, diff --git a/ErsatzTV/Controllers/Api/RerunCollectionController.cs b/ErsatzTV/Controllers/Api/RerunCollectionController.cs index ffb542d82..6faa54031 100644 --- a/ErsatzTV/Controllers/Api/RerunCollectionController.cs +++ b/ErsatzTV/Controllers/Api/RerunCollectionController.cs @@ -62,6 +62,13 @@ public class RerunCollectionController(IMediator mediator) : ControllerBase [Required] [FromBody] CreateRerunCollectionRequest request, CancellationToken cancellationToken) { + if (!RerunCollectionRequestMapping.IsSupportedSelectionType(request.CollectionType)) + { + return BaseError.New( + $"Unsupported collection type '{request.CollectionType}' for a rerun collection") + .ToErrorResult(); + } + Either result = await mediator.Send(request.ToCommand(), cancellationToken); return result.ToCreatedResult( @@ -81,6 +88,13 @@ public class RerunCollectionController(IMediator mediator) : ControllerBase [Required] [FromBody] UpdateRerunCollectionRequest request, CancellationToken cancellationToken) { + if (!RerunCollectionRequestMapping.IsSupportedSelectionType(request.CollectionType)) + { + return BaseError.New( + $"Unsupported collection type '{request.CollectionType}' for a rerun collection") + .ToErrorResult(); + } + Either result = await mediator.Send(request.ToCommand(id), cancellationToken); return await result.Match( Left: error => Task.FromResult(error.ToErrorResult()),