Add optional `int? Id` to ScheduleItemRequest/ReplaceProgramScheduleItem so a client can round-trip each existing item's server id. When ids are present, ReplaceProgramScheduleItemsHandler reconciles by id (not array position), so an item's persisted fill-group/shuffle state (PlayoutScheduleItemFillGroupIndex, FK OnDelete Cascade) follows the logical item across reorders/inserts instead of being inherited by whatever previously occupied its new slot (#259, split from #252/#253). A fully id-less payload keeps the verbatim positional fallback. Guards (inside PersistItems, after CheckVersion so 412 precedes 422): duplicate id -> 422; id not in this schedule -> 422 (a stale id under Phase-1 force-write is a live lost-update signal, not a new item). Index stays array-position derived. Regenerated v1.json + TS client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
190 lines
6.6 KiB
C#
190 lines
6.6 KiB
C#
using System.Threading.Channels;
|
|
using ErsatzTV.Application;
|
|
using ErsatzTV.Application.ProgramSchedules;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Scheduling;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Tests.Support;
|
|
using LanguageExt;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Integration;
|
|
|
|
[TestFixture]
|
|
public class ScheduleItemTptIntegrationTests
|
|
{
|
|
private InMemoryTvContext _db = null!;
|
|
private ChannelWriter<IBackgroundServiceRequest> _worker = null!;
|
|
|
|
[SetUp]
|
|
public async Task SetUp()
|
|
{
|
|
_db = await InMemoryTvContext.CreateAsync();
|
|
_worker = System.Threading.Channels.Channel.CreateUnbounded<IBackgroundServiceRequest>().Writer;
|
|
}
|
|
|
|
[TearDown]
|
|
public async Task TearDown() => await _db.DisposeAsync();
|
|
|
|
[Test]
|
|
public async Task AddProgramScheduleItem_Should_Create_Tpt_Subtype_Row_For_Each_PlayoutMode()
|
|
{
|
|
int scheduleId = await SeedSchedule();
|
|
var handler = new AddProgramScheduleItemHandler(_db.Factory, _worker);
|
|
|
|
foreach (PlayoutMode playoutMode in new[]
|
|
{
|
|
PlayoutMode.One,
|
|
PlayoutMode.Multiple,
|
|
PlayoutMode.Flood,
|
|
PlayoutMode.Duration
|
|
})
|
|
{
|
|
Either<BaseError, ProgramScheduleItemViewModel> result =
|
|
await handler.Handle(MakeAdd(scheduleId, playoutMode), CancellationToken.None);
|
|
|
|
result.IsRight.ShouldBeTrue();
|
|
}
|
|
|
|
await using TvContext context = _db.CreateContext();
|
|
(await CountRows(context, "ProgramScheduleOneItem")).ShouldBe(1);
|
|
(await CountRows(context, "ProgramScheduleMultipleItem")).ShouldBe(1);
|
|
(await CountRows(context, "ProgramScheduleFloodItem")).ShouldBe(1);
|
|
(await CountRows(context, "ProgramScheduleDurationItem")).ShouldBe(1);
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReplaceProgramScheduleItems_Should_Create_Tpt_Subtype_Rows_For_Replaced_Items()
|
|
{
|
|
int scheduleId = await SeedSchedule();
|
|
var handler = new ReplaceProgramScheduleItemsHandler(_db.Factory, _worker);
|
|
|
|
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> result =
|
|
await handler.Handle(
|
|
new ReplaceProgramScheduleItems(
|
|
scheduleId,
|
|
[
|
|
MakeReplace(0, PlayoutMode.One),
|
|
MakeReplace(1, PlayoutMode.Multiple),
|
|
MakeReplace(2, PlayoutMode.Flood),
|
|
MakeReplace(3, PlayoutMode.Duration)
|
|
]),
|
|
CancellationToken.None);
|
|
|
|
result.IsRight.ShouldBeTrue();
|
|
|
|
await using TvContext context = _db.CreateContext();
|
|
(await CountRows(context, "ProgramScheduleOneItem")).ShouldBe(1);
|
|
(await CountRows(context, "ProgramScheduleMultipleItem")).ShouldBe(1);
|
|
(await CountRows(context, "ProgramScheduleFloodItem")).ShouldBe(1);
|
|
(await CountRows(context, "ProgramScheduleDurationItem")).ShouldBe(1);
|
|
}
|
|
|
|
private async Task<int> SeedSchedule()
|
|
{
|
|
await using TvContext context = _db.CreateContext();
|
|
var schedule = new ProgramSchedule
|
|
{
|
|
Name = "Integration",
|
|
Items = [],
|
|
Playouts = [],
|
|
ProgramScheduleAlternates = []
|
|
};
|
|
context.ProgramSchedules.Add(schedule);
|
|
await context.SaveChangesAsync();
|
|
return schedule.Id;
|
|
}
|
|
|
|
private static AddProgramScheduleItem MakeAdd(int scheduleId, PlayoutMode playoutMode) =>
|
|
new(
|
|
scheduleId,
|
|
StartType.Dynamic,
|
|
StartTime: null,
|
|
FixedStartTimeBehavior: null,
|
|
playoutMode,
|
|
CollectionType.SearchQuery,
|
|
CollectionId: null,
|
|
MultiCollectionId: null,
|
|
SmartCollectionId: null,
|
|
RerunCollectionId: null,
|
|
MediaItemId: null,
|
|
PlaylistId: null,
|
|
SearchTitle: "News",
|
|
SearchQuery: "news",
|
|
PlaybackOrder: PlaybackOrder.Shuffle,
|
|
MarathonGroupBy: MarathonGroupBy.None,
|
|
MarathonShuffleGroups: false,
|
|
MarathonShuffleItems: false,
|
|
MarathonBatchSize: null,
|
|
FillWithGroupMode: FillWithGroupMode.None,
|
|
MultipleMode: MultipleMode.Count,
|
|
MultipleCount: "2",
|
|
PlayoutDuration: TimeSpan.FromMinutes(30),
|
|
TailMode: TailMode.None,
|
|
DiscardToFillAttempts: 3,
|
|
CustomTitle: null,
|
|
GuideMode: GuideMode.Normal,
|
|
PreRollFillerId: null,
|
|
MidRollFillerId: null,
|
|
PostRollFillerId: null,
|
|
TailFillerId: null,
|
|
FallbackFillerId: null,
|
|
WatermarkIds: [],
|
|
GraphicsElementIds: [],
|
|
PreferredAudioLanguageCode: null,
|
|
PreferredAudioTitle: null,
|
|
PreferredSubtitleLanguageCode: null,
|
|
SubtitleMode: null);
|
|
|
|
private static ReplaceProgramScheduleItem MakeReplace(int index, PlayoutMode playoutMode)
|
|
{
|
|
AddProgramScheduleItem add = MakeAdd(1, playoutMode);
|
|
return new ReplaceProgramScheduleItem(
|
|
null,
|
|
index,
|
|
add.StartType,
|
|
add.StartTime,
|
|
add.FixedStartTimeBehavior,
|
|
add.PlayoutMode,
|
|
add.CollectionType,
|
|
add.CollectionId,
|
|
add.MultiCollectionId,
|
|
add.SmartCollectionId,
|
|
add.RerunCollectionId,
|
|
add.MediaItemId,
|
|
add.PlaylistId,
|
|
add.SearchTitle,
|
|
add.SearchQuery,
|
|
add.PlaybackOrder,
|
|
add.MarathonGroupBy,
|
|
add.MarathonShuffleGroups,
|
|
add.MarathonShuffleItems,
|
|
add.MarathonBatchSize,
|
|
add.FillWithGroupMode,
|
|
add.MultipleMode,
|
|
add.MultipleCount,
|
|
add.PlayoutDuration,
|
|
add.TailMode,
|
|
add.DiscardToFillAttempts,
|
|
add.CustomTitle,
|
|
add.GuideMode,
|
|
add.PreRollFillerId,
|
|
add.MidRollFillerId,
|
|
add.PostRollFillerId,
|
|
add.TailFillerId,
|
|
add.FallbackFillerId,
|
|
add.WatermarkIds,
|
|
add.GraphicsElementIds,
|
|
add.PreferredAudioLanguageCode,
|
|
add.PreferredAudioTitle,
|
|
add.PreferredSubtitleLanguageCode,
|
|
add.SubtitleMode);
|
|
}
|
|
|
|
private static Task<int> CountRows(TvContext context, string tableName) =>
|
|
context.Database.SqlQueryRaw<int>($"SELECT COUNT(*) AS Value FROM {tableName}").SingleAsync();
|
|
}
|