Files
ersatztv/ErsatzTV.Tests/Support/ChannelHandlerTestBase.cs
T
timothyandClaude Fable 5.1 9fc54fed87 fix(568): the raw-count cap is the request's bound, and a recovery path re-asks the whole validator set
Two holes the review round found in the previous fix, both of the same shape: a
guard that names its own fields instead of deriving them.

The deco validators short-circuited the entire Validators.IdsMustExist call when
the DecoMode does not consume the ids, which took the 512-item raw-count cap with
it -- an arbitrarily large array under Inherit/Disable parsed and materialized
with nothing bounding it. Only the EXISTENCE half is the apply path's business,
so the mode predicate is now a required argument of the shared validator and gates
that half alone; the cap runs under every mode.

The channel recovery path rechecked GraphicsElementIdsMustExist alone, so a
watermark deleted between validation and SaveChangesAsync still surfaced as the
unhandled 500 the fix exists to remove -- WatermarkId, FFmpegProfileId,
FallbackFillerId and MirrorSourceChannelId are all written by the same save and
lose the same race. Both handlers now re-ask the whole of Validate on
DbUpdateException, so a validator added later is covered without editing the
recovery path.

The API-site outside-folder discriminator test seeded an Image row, so the Kind
conjunct rejected it whatever the path comparison did: a composite revert to
Path.GetFileName(path) == filename && kind == Text passed every API test. It now
carries the seeded Kind, mirroring the seeder-site twin, so only the path half can
reject it.

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

164 lines
5.2 KiB
C#

using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using NSubstitute;
using NUnit.Framework;
using DomainChannel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Tests.Support;
public abstract class ChannelHandlerTestBase
{
protected InMemoryTvContext Db = null!;
protected ChannelWriter<IBackgroundServiceRequest> Worker = null!;
protected ISearchTargets SearchTargets = null!;
protected IRemoteLogoCacher RemoteLogoCacher = null!;
[SetUp]
public async Task BaseSetUp()
{
Db = await InMemoryTvContext.CreateAsync();
Worker = System.Threading.Channels.Channel.CreateUnbounded<IBackgroundServiceRequest>().Writer;
SearchTargets = Substitute.For<ISearchTargets>();
RemoteLogoCacher = Substitute.For<IRemoteLogoCacher>();
}
[TearDown]
public async Task BaseTearDown() => await Db.DisposeAsync();
protected async Task SeedFFmpegProfile(int id = 1)
{
await using TvContext context = Db.CreateContext();
context.FFmpegProfiles.Add(new FFmpegProfile { Id = id, Name = $"profile-{id}" });
await context.SaveChangesAsync();
}
protected async Task<DomainChannel> SeedChannel(
int id,
string number,
string name = "Test",
int ffmpegProfileId = 1,
string group = "ErsatzTV")
{
await using TvContext context = Db.CreateContext();
var channel = new DomainChannel(Guid.NewGuid())
{
Id = id,
Number = number,
Name = name,
Group = group,
Categories = string.Empty,
FFmpegProfileId = ffmpegProfileId,
StreamSelector = string.Empty,
PreferredAudioLanguageCode = string.Empty,
PreferredAudioTitle = string.Empty,
PreferredSubtitleLanguageCode = string.Empty,
MusicVideoCreditsTemplate = string.Empty,
StreamingMode = StreamingMode.TransportStreamHybrid,
PlayoutSource = ChannelPlayoutSource.Generated,
PlayoutMode = ChannelPlayoutMode.Continuous
};
context.Channels.Add(channel);
await context.SaveChangesAsync();
return channel;
}
protected async Task SeedPlayout(int id, int channelId)
{
await using TvContext context = Db.CreateContext();
context.Playouts.Add(
new Playout
{
Id = id,
ChannelId = channelId,
ScheduleKind = PlayoutScheduleKind.Classic
});
await context.SaveChangesAsync();
}
protected static CreateChannel MakeCreate(
string number = "5",
int ffmpegProfileId = 1,
bool isEnabled = true,
bool showInEpg = false,
string logoPath = "",
string name = "Test",
string group = "ErsatzTV",
StreamingMode streamingMode = StreamingMode.TransportStreamHybrid) =>
new(
name,
number,
group,
string.Empty,
ffmpegProfileId,
null,
new ArtworkContentTypeModel(logoPath, string.Empty),
ChannelStreamSelectorMode.Default,
string.Empty,
string.Empty,
string.Empty,
ChannelPlayoutSource.Generated,
ChannelPlayoutMode.Continuous,
null,
null,
streamingMode,
null,
null,
string.Empty,
ChannelSubtitleMode.None,
ChannelMusicVideoCreditsMode.None,
string.Empty,
ChannelSongVideoMode.Default,
ChannelTranscodeMode.OnDemand,
ChannelIdleBehavior.StopOnDisconnect,
isEnabled,
showInEpg);
protected static UpdateChannel MakeUpdate(
int channelId,
string number = "5",
int ffmpegProfileId = 1,
bool isEnabled = true,
bool showInEpg = false,
string logoPath = "",
string name = "Test",
string group = "ErsatzTV",
List<int> graphicsElementIds = null,
int? watermarkId = null) =>
new(
channelId,
name,
number,
group,
string.Empty,
ffmpegProfileId,
null,
new ArtworkContentTypeModel(logoPath, string.Empty),
ChannelStreamSelectorMode.Default,
string.Empty,
string.Empty,
string.Empty,
ChannelPlayoutSource.Generated,
ChannelPlayoutMode.Continuous,
null,
null,
StreamingMode.TransportStreamHybrid,
watermarkId,
null,
string.Empty,
ChannelSubtitleMode.None,
ChannelMusicVideoCreditsMode.None,
string.Empty,
ChannelSongVideoMode.Default,
ChannelTranscodeMode.OnDemand,
ChannelIdleBehavior.StopOnDisconnect,
isEnabled,
showInEpg,
graphicsElementIds ?? []);
}