Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10d891ba63 | ||
|
|
a3080ca42b |
@@ -52,16 +52,6 @@ public class UpdateChannelHandler(
|
||||
UpdateChannel update,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// don't save mirror when playout exists
|
||||
if (c.Playouts.Count > 0)
|
||||
{
|
||||
update = update with
|
||||
{
|
||||
PlayoutSource = ChannelPlayoutSource.Generated,
|
||||
MirrorSourceChannelId = null
|
||||
};
|
||||
}
|
||||
|
||||
bool hasEpgChange = c.PlayoutSource != update.PlayoutSource || c.ShowInEpg != update.ShowInEpg;
|
||||
|
||||
c.Name = update.Name;
|
||||
@@ -140,6 +130,8 @@ public class UpdateChannelHandler(
|
||||
c.PlayoutMode = ChannelPlayoutMode.Continuous;
|
||||
hasEpgChange |= c.MirrorSourceChannelId != update.MirrorSourceChannelId;
|
||||
hasEpgChange |= c.PlayoutOffset != update.PlayoutOffset;
|
||||
c.MirrorSourceChannelId = update.MirrorSourceChannelId;
|
||||
c.PlayoutOffset = update.PlayoutOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -147,8 +139,6 @@ public class UpdateChannelHandler(
|
||||
c.PlayoutOffset = null;
|
||||
}
|
||||
|
||||
c.MirrorSourceChannelId = update.MirrorSourceChannelId;
|
||||
c.PlayoutOffset = update.PlayoutOffset;
|
||||
c.StreamingMode = update.StreamingMode;
|
||||
c.WatermarkId = update.WatermarkId;
|
||||
c.FallbackFillerId = update.FallbackFillerId;
|
||||
@@ -194,7 +184,7 @@ public class UpdateChannelHandler(
|
||||
{
|
||||
Validation<BaseError, Channel> channelValidation = (ValidateName(request),
|
||||
await ValidateNumber(dbContext, request, cancellationToken),
|
||||
await MirrorSourceMustBeValid(dbContext, request, cancellationToken),
|
||||
await MirrorSourceMustBeValid(dbContext, request, channel, cancellationToken),
|
||||
ValidateShowInEpg(request.IsEnabled, request.ShowInEpg),
|
||||
ValidateLogo(request.Logo?.Path))
|
||||
.Apply((_, _, _, _, _) => channel);
|
||||
@@ -269,6 +259,7 @@ public class UpdateChannelHandler(
|
||||
private static async Task<Validation<BaseError, Unit>> MirrorSourceMustBeValid(
|
||||
TvContext dbContext,
|
||||
UpdateChannel request,
|
||||
Channel channel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.PlayoutSource is not ChannelPlayoutSource.Mirror)
|
||||
@@ -276,6 +267,18 @@ public class UpdateChannelHandler(
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
// a channel with its own playout already built (Generated mode) cannot become a Mirror —
|
||||
// Mirror channels relay another channel's playout and never build one of their own, so
|
||||
// switching this transition on would strand the existing playout. This used to be
|
||||
// silently coerced back to Generated (issue #401); reject the transition instead so the
|
||||
// caller sees why the requested Mirror source was not applied. A round-trip that keeps
|
||||
// PlayoutSource as Generated never reaches this check.
|
||||
if (channel.Playouts.Count > 0)
|
||||
{
|
||||
return BaseError.New(
|
||||
"Channel cannot switch to Mirror playout source while it has a playout; reset or delete the existing playout first.");
|
||||
}
|
||||
|
||||
Option<Channel> maybeMirrorSource = await dbContext.Channels
|
||||
.AsNoTracking()
|
||||
.SelectOneAsync(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Application.Channels;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Tests.Support;
|
||||
@@ -114,6 +115,77 @@ public class UpdateChannelHandlerTests : ChannelHandlerTestBase
|
||||
error.Value.ShouldContain("FFmpegProfile");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Reject_Mirror_Transition_When_Channel_Has_Playout()
|
||||
{
|
||||
await SeedFFmpegProfile();
|
||||
await SeedChannel(1, "5"); // has a playout below
|
||||
await SeedChannel(2, "6"); // valid mirror source (Generated, no playouts of its own)
|
||||
await SeedPlayout(1, channelId: 1);
|
||||
|
||||
UpdateChannel update = MakeUpdate(1, number: "5") with
|
||||
{
|
||||
PlayoutSource = ChannelPlayoutSource.Mirror,
|
||||
MirrorSourceChannelId = 2
|
||||
};
|
||||
|
||||
Either<BaseError, ChannelViewModel> result = await MakeHandler().Handle(update, CancellationToken.None);
|
||||
|
||||
BaseError error = LeftOf(result);
|
||||
error.ShouldNotBeOfType<NotFoundError>();
|
||||
error.Value.ShouldContain("Mirror");
|
||||
|
||||
// the channel must NOT have been silently coerced/saved as Generated (issue #401: no
|
||||
// silent 200, the caller's requested transition is rejected outright)
|
||||
await using TvContext context = Db.CreateContext();
|
||||
var channel = await context.Channels.SingleAsync(c => c.Id == 1);
|
||||
channel.PlayoutSource.ShouldBe(ChannelPlayoutSource.Generated);
|
||||
channel.MirrorSourceChannelId.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Allow_GetPut_Roundtrip_Of_Generated_Channel_With_Playout()
|
||||
{
|
||||
await SeedFFmpegProfile();
|
||||
await SeedChannel(1, "5");
|
||||
await SeedPlayout(1, channelId: 1);
|
||||
|
||||
// client GETs the channel (PlayoutSource: Generated) and PUTs the same value back
|
||||
// unchanged; this must still succeed even though the channel has a playout.
|
||||
Either<BaseError, ChannelViewModel> result =
|
||||
await MakeHandler().Handle(MakeUpdate(1, number: "5", name: "Renamed"), CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Null_Mirror_Only_Fields_When_Saving_Generated_Channel_With_Playout()
|
||||
{
|
||||
await SeedFFmpegProfile();
|
||||
await SeedChannel(1, "5"); // has a playout below
|
||||
await SeedChannel(2, "6"); // stray reference target
|
||||
await SeedPlayout(1, channelId: 1);
|
||||
|
||||
// request keeps PlayoutSource: Generated but carries stray Mirror-only fields — e.g. a
|
||||
// client that never cleared the fields after flipping the UI back from Mirror. These
|
||||
// must never persist onto a Generated channel.
|
||||
UpdateChannel update = MakeUpdate(1, number: "5") with
|
||||
{
|
||||
MirrorSourceChannelId = 2,
|
||||
PlayoutOffset = TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
Either<BaseError, ChannelViewModel> result = await MakeHandler().Handle(update, CancellationToken.None);
|
||||
|
||||
result.IsRight.ShouldBeTrue();
|
||||
|
||||
await using TvContext context = Db.CreateContext();
|
||||
var channel = await context.Channels.SingleAsync(c => c.Id == 1);
|
||||
channel.PlayoutSource.ShouldBe(ChannelPlayoutSource.Generated);
|
||||
channel.MirrorSourceChannelId.ShouldBeNull();
|
||||
channel.PlayoutOffset.ShouldBeNull();
|
||||
}
|
||||
|
||||
private static BaseError LeftOf<TR>(Either<BaseError, TR> either) =>
|
||||
either.Match(Left: e => e, Right: _ => throw new AssertionException("Expected a Left result"));
|
||||
}
|
||||
|
||||
@@ -64,6 +64,19 @@ public abstract class ChannelHandlerTestBase
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user