Files
ersatztv/ErsatzTV.Tests/Application/Channels/CreateChannelDefaultGraphicsElementTests.cs
T
timothyandClaude Fable 5.1 900dac9832 fix(568): reject unknown channel graphicsElementIds with 422; discriminate builtIn by seeded path not filename
UpdateChannelHandler.Validate never checked incoming graphicsElementIds against
GraphicsElements, so PUT /api/v1/channels/{id} with a non-existent id hit
FK_ChannelGraphicsElement_GraphicsElement_GraphicsElementId at SaveChangesAsync
and surfaced as an unhandled 500. Add GraphicsElementIdsMustExist, following the
existing FFmpegProfileMustExist/WatermarkMustExist/FillerPresetMustExist shape,
so an unknown id now returns 422 for parity with every other FK field on this
full-replace DTO.

GetAllGraphicsElementsForApiHandler and GraphicsElementSeeder.GetBuiltInElementId
keyed builtIn off Path.GetFileName(e.Path) == OnNowNextFileName -- folder-agnostic,
so a user element named exactly on-now-next.yml in any other template folder would
also report builtIn:true. Both now compare against
GraphicsElementDefaults.OnNowNextSeededPath, the full path the seeder actually
writes to.

Follow-up from the #74 whole-branch review (2026-07-22), deferred as
data-safe/not SPA-reachable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 21:35:14 +02:00

93 lines
3.0 KiB
C#

using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Graphics;
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Application.Channels;
/// <summary>
/// #732: the On Now / Next overlay is a default rather than an opt-in, so a channel created after
/// that decision gets the built-in element without the operator toggling anything.
/// </summary>
[TestFixture]
public class CreateChannelDefaultGraphicsElementTests : ChannelHandlerTestBase
{
private CreateChannelHandler MakeHandler() => new(Worker, Db.Factory, SearchTargets, RemoteLogoCacher);
private async Task<int> SeedBuiltInElement()
{
await using TvContext context = Db.CreateContext();
var element = new GraphicsElement
{
Path = GraphicsElementDefaults.OnNowNextSeededPath,
Kind = GraphicsElementKind.Text
};
context.GraphicsElements.Add(element);
await context.SaveChangesAsync();
return element.Id;
}
private async Task<List<int>> AttachedElementIds(int channelId)
{
await using TvContext context = Db.CreateContext();
Channel reloaded = await context.Channels
.Include(c => c.ChannelGraphicsElements)
.SingleAsync(c => c.Id == channelId);
return reloaded.ChannelGraphicsElements.Select(x => x.GraphicsElementId).ToList();
}
[Test]
public async Task Attaches_The_Built_In_Element_To_A_New_Channel()
{
await SeedFFmpegProfile();
int elementId = await SeedBuiltInElement();
Either<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(MakeCreate(), CancellationToken.None);
result.IsRight.ShouldBeTrue();
int channelId = result.RightToSeq().Head().ChannelId;
(await AttachedElementIds(channelId)).ShouldBe([elementId]);
}
[Test]
public async Task Leaves_An_Hls_Direct_Channel_Alone_Because_Nothing_Can_Render_There()
{
await SeedFFmpegProfile();
await SeedBuiltInElement();
Either<BaseError, CreateChannelResult> result = await MakeHandler().Handle(
MakeCreate(streamingMode: StreamingMode.HttpLiveStreamingDirect),
CancellationToken.None);
result.IsRight.ShouldBeTrue();
int channelId = result.RightToSeq().Head().ChannelId;
(await AttachedElementIds(channelId)).ShouldBeEmpty();
}
[Test]
public async Task Creates_The_Channel_Even_When_The_Built_In_Element_Does_Not_Exist()
{
await SeedFFmpegProfile();
Either<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(MakeCreate(), CancellationToken.None);
result.IsRight.ShouldBeTrue();
int channelId = result.RightToSeq().Head().ChannelId;
(await AttachedElementIds(channelId)).ShouldBeEmpty();
}
}