using TeleWave.Application.Streaming.GetIptvGuide; using TeleWave.Application.Tests.Support; using TeleWave.Domain.Broadcast; using TeleWave.Domain.Library; using TeleWave.Domain.Media; using Xunit; namespace TeleWave.Application.Tests.Streaming; /// /// Телегид для IPTV: программа всех включённых каналов одной выгрузкой. Проверяем то, чем он /// отличается от списка записей ленты — склейку серий в блок, отбор окна и подстановку описаний. /// public class IptvGuideTests { private static readonly DateTimeOffset T0 = new(2026, 1, 1, 12, 0, 0, TimeSpan.Zero); [Fact] public async Task Guide_MergesConsecutiveEpisodesOfTheSameShow() { var fixture = new TestDb(); var channel = Enabled("Первый", "one"); var show = Show.Create("Футурама", ShowKind.Series); var first = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload); var second = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload); show.AddEpisode(first.Id); show.AddEpisode(second.Id); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Shows.Add(show); seed.MediaAssets.AddRange(first, second); seed.ScheduleEntries.AddRange( Program(channel.Id, first.Id, show.Id, T0, 25), Program(channel.Id, second.Id, show.Id, T0.AddMinutes(25), 25) ); await seed.SaveChangesAsync(CancellationToken.None); } var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3)); // Две серии встык — одна передача в глазах зрителя: гид показывает программу, а не файлы. var programme = Assert.Single(Assert.Single(channels).Programmes); Assert.Equal("Футурама", programme.Title); Assert.Equal(T0, programme.StartsAtUtc); Assert.Equal(T0.AddMinutes(50), programme.EndsAtUtc); } [Fact] public async Task Guide_KeepsBlocksApartWhenSomethingRanBetween() { var fixture = new TestDb(); var channel = Enabled("Первый", "one"); var show = Show.Create("Футурама", ShowKind.Series); var first = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload); var second = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload); show.AddEpisode(first.Id); show.AddEpisode(second.Id); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Shows.Add(show); seed.MediaAssets.AddRange(first, second); seed.ScheduleEntries.AddRange( Program(channel.Id, first.Id, show.Id, T0, 25), // Разрыв в пять минут — между сериями шла реклама, и одной передачей это не назвать. Program(channel.Id, second.Id, show.Id, T0.AddMinutes(30), 25) ); await seed.SaveChangesAsync(CancellationToken.None); } var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3)); var programmes = Assert.Single(channels).Programmes; Assert.Equal(2, programmes.Count); Assert.Equal(T0.AddMinutes(25), programmes[0].EndsAtUtc); Assert.Equal(T0.AddMinutes(30), programmes[1].StartsAtUtc); } [Fact] public async Task Guide_TakesTitleAndCoverFromEpisodeThenFallsBackToShow() { var fixture = new TestDb(); var channel = Enabled("Первый", "one"); var stillId = Guid.NewGuid(); var posterId = Guid.NewGuid(); var titled = Show.Create("Футурама", ShowKind.Series); titled.SetPosterImage(posterId); var withMetadata = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload); var bare = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload); titled .AddEpisode(withMetadata.Id) .ApplyMetadata("Космический пилот", "Фрай", stillId, null); titled.AddEpisode(bare.Id); await using (var seed = fixture.New()) { seed.Channels.Add(channel); seed.Shows.Add(titled); seed.MediaAssets.AddRange(withMetadata, bare); seed.ScheduleEntries.AddRange( Program(channel.Id, withMetadata.Id, titled.Id, T0, 25), Program(channel.Id, bare.Id, titled.Id, T0.AddMinutes(30), 25) ); await seed.SaveChangesAsync(CancellationToken.None); } var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3)); var programmes = Assert.Single(channels).Programmes; Assert.Equal("Космический пилот", programmes[0].SubTitle); Assert.Equal("Фрай", programmes[0].Description); Assert.Equal(stillId, programmes[0].ImageId); // Серия без метаданных показывается обложкой шоу — пустая карточка в плеере хуже общей. Assert.Null(programmes[1].SubTitle); Assert.Equal(posterId, programmes[1].ImageId); } [Fact] public async Task Guide_TakesOnlyEnabledChannelsAndTheRequestedWindow() { var fixture = new TestDb(); var visible = Enabled("Первый", "one"); var hidden = Channel.Create("Выключенный", "off", T0); hidden.UpdateSettings("Выключенный", isEnabled: false, null); var show = Show.Create("Ворон", ShowKind.Single); var asset = MediaAsset.Register("crow.mkv", ".mkv", MediaSource.Upload); show.AddEpisode(asset.Id); await using (var seed = fixture.New()) { seed.Channels.AddRange(visible, hidden); seed.Shows.Add(show); seed.MediaAssets.Add(asset); seed.ScheduleEntries.AddRange( Program(visible.Id, asset.Id, show.Id, T0, 60), // За окном — не в выгрузке; выключенный канал не показываем вовсе. Program(visible.Id, asset.Id, show.Id, T0.AddHours(10), 60), Program(hidden.Id, asset.Id, show.Id, T0, 60), // Заставки и врезки в гид не идут: у них нет шоу, а значит и передачи. ScheduleEntry.Bumper( visible.Id, asset.Id, T0.AddHours(2), T0.AddHours(2).AddSeconds(10), null, null ) ); await seed.SaveChangesAsync(CancellationToken.None); } var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3)); var dto = Assert.Single(channels); Assert.Equal("one", dto.Slug); var programme = Assert.Single(dto.Programmes); Assert.Equal("Ворон", programme.Title); } [Fact] public async Task Guide_WithoutChannels_ReturnsEmpty() { var fixture = new TestDb(); var channels = await GuideAsync(fixture, T0, T0.AddHours(3)); Assert.Empty(channels); } private static async Task> GuideAsync( TestDb fixture, DateTimeOffset from, DateTimeOffset to ) { await using var db = fixture.New(); return await new GetIptvGuideQueryHandler(db).Handle( new GetIptvGuideQuery(from, to), CancellationToken.None ); } private static Channel Enabled(string name, string slug) { var channel = Channel.Create(name, slug, T0); channel.UpdateSettings(name, isEnabled: true, null); return channel; } private static ScheduleEntry Program( Guid channelId, Guid assetId, Guid showId, DateTimeOffset startsAt, int minutes ) => ScheduleEntry.Program( channelId, assetId, startsAt, startsAt.AddMinutes(minutes), showId, 0 ); }