Files
ersatztv/ErsatzTV.Core.Tests/FFmpeg/WatermarkSelectorChannelLogoTests.cs
T

208 lines
7.8 KiB
C#

using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.FFmpeg;
/// <summary>
/// Covers <see cref="ChannelWatermarkImageSource.ChannelLogo" /> resolution at all three watermark
/// precedence levels (playout item, channel, global). The shared fixture in
/// <see cref="WatermarkSelectorTests" /> deliberately makes every watermark file exist, so it cannot
/// express the "logo is an external URL" or "logo file is gone" cases this fixture exists for (#502).
/// </summary>
[TestFixture]
public class WatermarkSelectorChannelLogoTests
{
private const string ExternalLogoUrl = "https://cdn.example.com/logos/channel.png";
private const string LocalLogoPath = "abc123.png";
private const string LocalLogoCachePath = "/cache/logos/ab/abc123.png";
private WatermarkSelector _selector;
[SetUp]
public void SetUp()
{
var mockFileSystem = new MockFileSystem();
mockFileSystem.Initialize().WithFile(LocalLogoCachePath);
var fakeImageCache = Substitute.For<IImageCache>();
fakeImageCache.GetPathForImage(Arg.Any<string>(), Arg.Is(ArtworkKind.Logo), Arg.Any<Option<int>>())
.Returns(_ => LocalLogoCachePath);
_selector = new WatermarkSelector(
mockFileSystem,
fakeImageCache,
Substitute.For<IDecoSelector>(),
NullLogger<WatermarkSelector>.Instance);
}
private static ChannelWatermark ChannelLogoWatermark(int id, string name) =>
new()
{
Id = id,
Name = name,
ImageSource = ChannelWatermarkImageSource.ChannelLogo,
Mode = ChannelWatermarkMode.Permanent
};
private static Channel ChannelWithLogo(string logoPath, ChannelWatermark channelWatermark = null)
{
var channel = new Channel(Guid.Empty)
{
Id = 1,
Number = "1",
Name = "Test",
StreamingMode = StreamingMode.TransportStream,
Artwork = [],
Watermark = channelWatermark,
WatermarkId = channelWatermark?.Id
};
if (logoPath is not null)
{
channel.Artwork.Add(new Artwork { ArtworkKind = ArtworkKind.Logo, Path = logoPath });
}
return channel;
}
// ---- external URL logo: render path must degrade to no bug, never fetch (#525) --------------
//
// As of #525 an external-URL logo is downloaded and cached at save time, so a URL path can only be a
// row that failed migration. The render/watermark path must NOT fetch at compositing time: it degrades
// to None (no on-screen bug) with a warning, rather than handing the URL downstream as a renderable
// ImagePath (the #502 behavior these tests previously pinned).
[Test]
public void PlayoutItemWatermark_Should_Ignore_External_Url_Channel_Logo()
{
ChannelWatermark watermark = ChannelLogoWatermark(1, "PlayoutItem");
Channel channel = ChannelWithLogo(ExternalLogoUrl);
Option<WatermarkOptions> result = _selector.GetWatermarkOptions(
channel,
watermark,
Option<ChannelWatermark>.None);
result.IsNone.ShouldBeTrue();
}
[Test]
public void ChannelWatermark_Should_Ignore_External_Url_Channel_Logo()
{
ChannelWatermark watermark = ChannelLogoWatermark(2, "Channel");
Channel channel = ChannelWithLogo(ExternalLogoUrl, watermark);
Option<WatermarkOptions> result = _selector.GetWatermarkOptions(
channel,
Option<ChannelWatermark>.None,
Option<ChannelWatermark>.None);
result.IsNone.ShouldBeTrue();
// never hand the URL downstream as a renderable path
result.IfSome(o => o.ImagePath.ShouldNotBe(ExternalLogoUrl));
}
[Test]
public void GlobalWatermark_Should_Ignore_External_Url_Channel_Logo()
{
ChannelWatermark watermark = ChannelLogoWatermark(3, "Global");
Channel channel = ChannelWithLogo(ExternalLogoUrl);
Option<WatermarkOptions> result = _selector.GetWatermarkOptions(
channel,
Option<ChannelWatermark>.None,
watermark);
result.IsNone.ShouldBeTrue();
}
/// <summary>
/// Scheme comparison goes through <see cref="Uri" />, which lower-cases it. Pinned because the fix
/// turns on <c>Artwork.IsExternalUrl</c>, and a case-sensitive check would silently fall back to the
/// existence-gated branch and re-introduce the defect for an oddly-cased URL.
/// </summary>
[Test]
public void ChannelWatermark_Should_Ignore_External_Url_Channel_Logo_Regardless_Of_Scheme_Case()
{
const string UpperCaseUrl = "HTTPS://cdn.example.com/logos/channel.png";
ChannelWatermark watermark = ChannelLogoWatermark(2, "Channel");
Channel channel = ChannelWithLogo(UpperCaseUrl, watermark);
Option<WatermarkOptions> result = _selector.GetWatermarkOptions(
channel,
Option<ChannelWatermark>.None,
Option<ChannelWatermark>.None);
result.IsNone.ShouldBeTrue();
}
// ---- regressions: local-file behavior must not change ---------------------------------------
[Test]
public void ChannelWatermark_Should_Use_Cached_Path_For_Local_Channel_Logo()
{
ChannelWatermark watermark = ChannelLogoWatermark(2, "Channel");
Channel channel = ChannelWithLogo(LocalLogoPath, watermark);
Option<WatermarkOptions> result = _selector.GetWatermarkOptions(
channel,
Option<ChannelWatermark>.None,
Option<ChannelWatermark>.None);
result.IsSome.ShouldBeTrue();
result.IfNone(() => throw new InvalidOperationException()).ImagePath.ShouldBe(LocalLogoCachePath);
}
[Test]
public void ChannelWatermark_Should_Be_Ignored_When_Local_Channel_Logo_File_Is_Missing()
{
var mockFileSystem = new MockFileSystem(); // nothing on disk
var fakeImageCache = Substitute.For<IImageCache>();
fakeImageCache.GetPathForImage(Arg.Any<string>(), Arg.Is(ArtworkKind.Logo), Arg.Any<Option<int>>())
.Returns(_ => LocalLogoCachePath);
var selector = new WatermarkSelector(
mockFileSystem,
fakeImageCache,
Substitute.For<IDecoSelector>(),
NullLogger<WatermarkSelector>.Instance);
ChannelWatermark watermark = ChannelLogoWatermark(2, "Channel");
Channel channel = ChannelWithLogo(LocalLogoPath, watermark);
Option<WatermarkOptions> result = selector.GetWatermarkOptions(
channel,
Option<ChannelWatermark>.None,
Option<ChannelWatermark>.None);
result.IsNone.ShouldBeTrue();
}
/// <summary>
/// Scope guard for #502: with no logo artwork at all, the resolved path is the generated-initials
/// URL from <see cref="Images.ChannelLogoGenerator.GenerateChannelLogoUrl" />, which hardcodes
/// localhost (issue #1). That fallback stays disabled here — reviving it is deliberately deferred
/// in docs/decisions.md and is not part of this fix.
/// </summary>
[Test]
public void ChannelWatermark_Should_Be_Ignored_When_Channel_Has_No_Logo_Artwork()
{
ChannelWatermark watermark = ChannelLogoWatermark(2, "Channel");
Channel channel = ChannelWithLogo(null, watermark);
Option<WatermarkOptions> result = _selector.GetWatermarkOptions(
channel,
Option<ChannelWatermark>.None,
Option<ChannelWatermark>.None);
result.IsNone.ShouldBeTrue();
}
}