Files
ersatztv/ErsatzTV.Tests/Application/Channels/CreateChannelHandlerTests.cs
T
timothyandtimothy 8b9a7ed541
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m23s
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Successful in 14m21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 19m9s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 12m1s
feat(414): stamp immutable Channel.Origin (auto-tuned vs user-created) and surface it (#575)
Co-authored-by: Timothy <timothy.look@gmail.com>
Co-committed-by: Timothy <timothy.look@gmail.com>
2026-07-23 18:12:25 +00:00

163 lines
5.7 KiB
C#

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<BaseError, CreateChannelResult> 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<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(MakeCreate(number: "5"), CancellationToken.None);
BaseError error = LeftOf(result);
error.ShouldNotBeOfType<NotFoundError>();
error.Value.ShouldContain("unique");
}
[Test]
public async Task Should_Reject_ShowInEpg_When_Disabled()
{
await SeedFFmpegProfile();
Either<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(
MakeCreate(number: "8", isEnabled: false, showInEpg: true),
CancellationToken.None);
BaseError error = LeftOf(result);
error.ShouldNotBeOfType<NotFoundError>();
error.Value.ShouldContain("EPG");
}
[Test]
public async Task Should_Reject_Invalid_External_Logo_Url()
{
await SeedFFmpegProfile();
Either<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(
MakeCreate(number: "9", logoPath: "ftp://example.com/logo.png"),
CancellationToken.None);
BaseError error = LeftOf(result);
error.ShouldNotBeOfType<NotFoundError>();
error.Value.ShouldContain("logo");
}
[Test]
public async Task Should_Reject_Empty_Group()
{
await SeedFFmpegProfile();
Either<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(MakeCreate(number: "11", group: " "), CancellationToken.None);
BaseError error = LeftOf(result);
error.ShouldNotBeOfType<NotFoundError>();
error.Value.ShouldContain("group");
}
[Test]
public async Task Should_Reject_Nonexistent_FFmpegProfile()
{
// intentionally do not seed an FFmpegProfile
Either<BaseError, CreateChannelResult> result =
await MakeHandler().Handle(MakeCreate(number: "12", ffmpegProfileId: 999), CancellationToken.None);
BaseError error = LeftOf(result);
error.ShouldNotBeOfType<NotFoundError>();
error.Value.ShouldContain("FFmpegProfile");
}
[Test]
public async Task Should_Download_External_Url_Logo_And_Store_Cache_Name()
{
await SeedFFmpegProfile();
RemoteLogoCacher.CacheFromUrl(Arg.Any<Uri>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, string>("cachedhash"));
Either<BaseError, CreateChannelResult> 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<Uri>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, string>(BaseError.New("Could not download logo: host timed out")));
Either<BaseError, CreateChannelResult> 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<BaseError, CreateChannelResult> result = await MakeHandler().Handle(
MakeCreate(number: "22", logoPath: "iptv/logos/deadbeef"),
CancellationToken.None);
result.IsRight.ShouldBeTrue();
await RemoteLogoCacher.DidNotReceive().CacheFromUrl(Arg.Any<Uri>(), Arg.Any<CancellationToken>());
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<TR>(Either<BaseError, TR> either) =>
either.Match(Left: e => e, Right: _ => throw new AssertionException("Expected a Left result"));
}