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
164 lines
5.2 KiB
C#
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 ?? []);
|
|
}
|