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; using Microsoft.EntityFrameworkCore; using NSubstitute; using NUnit.Framework; using Shouldly; using static LanguageExt.Prelude; namespace ErsatzTV.Tests.Application.Channels; [TestFixture] public class CreateChannelHandlerTests : ChannelHandlerTestBase { private CreateChannelHandler MakeHandler() => new(Worker, Db.Factory, SearchTargets, RemoteLogoCacher); [Test] public async Task Should_Create_Channel_When_Valid() { await SeedFFmpegProfile(); Either result = await MakeHandler().Handle(MakeCreate(number: "7", name: "News"), CancellationToken.None); result.IsRight.ShouldBeTrue(); await using TvContext context = Db.CreateContext(); Channel channel = await context.Channels.SingleAsync(c => c.Number == "7" && c.Name == "News"); channel.Origin.ShouldBe(ChannelOrigin.UserCreated); } [Test] public async Task Should_Reject_Duplicate_Number_With_422_Error() { await SeedFFmpegProfile(); await SeedChannel(1, "5"); Either result = await MakeHandler().Handle(MakeCreate(number: "5"), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("unique"); } [Test] public async Task Should_Reject_ShowInEpg_When_Disabled() { await SeedFFmpegProfile(); Either result = await MakeHandler().Handle( MakeCreate(number: "8", isEnabled: false, showInEpg: true), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("EPG"); } [Test] public async Task Should_Reject_Invalid_External_Logo_Url() { await SeedFFmpegProfile(); Either result = await MakeHandler().Handle( MakeCreate(number: "9", logoPath: "ftp://example.com/logo.png"), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("logo"); } [Test] public async Task Should_Reject_Empty_Group() { await SeedFFmpegProfile(); Either result = await MakeHandler().Handle(MakeCreate(number: "11", group: " "), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("group"); } [Test] public async Task Should_Reject_Nonexistent_FFmpegProfile() { // intentionally do not seed an FFmpegProfile Either result = await MakeHandler().Handle(MakeCreate(number: "12", ffmpegProfileId: 999), CancellationToken.None); BaseError error = LeftOf(result); error.ShouldNotBeOfType(); error.Value.ShouldContain("FFmpegProfile"); } [Test] public async Task Should_Download_External_Url_Logo_And_Store_Cache_Name() { await SeedFFmpegProfile(); RemoteLogoCacher.CacheFromUrl(Arg.Any(), Arg.Any()) .Returns(Right("cachedhash")); Either result = await MakeHandler().Handle( MakeCreate(number: "20", logoPath: "https://example.com/logo.png"), CancellationToken.None); result.IsRight.ShouldBeTrue(); await using TvContext db = Db.CreateContext(); Artwork logo = db.Channels.Include(c => c.Artwork).Single(c => c.Number == "20") .Artwork.Single(a => a.ArtworkKind == ArtworkKind.Logo); logo.Path.ShouldBe("cachedhash"); logo.IsExternalUrl().ShouldBeFalse(); } [Test] public async Task Should_Fail_The_Create_When_The_Logo_Download_Fails() { await SeedFFmpegProfile(); RemoteLogoCacher.CacheFromUrl(Arg.Any(), Arg.Any()) .Returns(Left(BaseError.New("Could not download logo: host timed out"))); Either result = await MakeHandler().Handle( MakeCreate(number: "21", logoPath: "https://example.com/logo.png"), CancellationToken.None); result.IsLeft.ShouldBeTrue(); LeftOf(result).Value.ShouldContain("Could not download logo"); await using TvContext db = Db.CreateContext(); (await db.Channels.AnyAsync(c => c.Number == "21")).ShouldBeFalse(); } [Test] public async Task Should_Not_Call_The_Cacher_For_An_Uploaded_Logo_Path() { await SeedFFmpegProfile(); Either result = await MakeHandler().Handle( MakeCreate(number: "22", logoPath: "iptv/logos/deadbeef"), CancellationToken.None); result.IsRight.ShouldBeTrue(); await RemoteLogoCacher.DidNotReceive().CacheFromUrl(Arg.Any(), Arg.Any()); await using TvContext db = Db.CreateContext(); Artwork logo = db.Channels.Include(c => c.Artwork).Single(c => c.Number == "22") .Artwork.Single(a => a.ArtworkKind == ArtworkKind.Logo); logo.Path.ShouldBe("deadbeef"); } private static BaseError LeftOf(Either either) => either.Match(Left: e => e, Right: _ => throw new AssertionException("Expected a Left result")); }