Files
ersatztv/ErsatzTV.Tests/Application/Channels/ChannelPreviewTests.cs
T
timothyandClaude Opus 4.8 f8ae4d62ab fix(552): mint a short-lived JWT so the SPA reaches /iptv/* under JWT auth
Under a JWT-enabled deployment (JWT:IssuerSigningKey set), /iptv/* is gated by
ConditionalIptvAuthorizeFilter and the "jwt" scheme does not accept the SPA's
ctv-session cookie, and nothing minted a JWT for the browser. So the #60 channel
preview was declared Unavailable and could not run at all.

Add GET /api/v1/auth/iptv-token (session-gated, on the [IgnoreApi] AuthController):
mints a short-lived global token via JwtHelper.GenerateBrowserToken (60 min default,
JWT:BrowserTokenLifetimeMinutes override), 204 when JWT is disabled. The SPA's new
withIptvToken(url) helper appends it as ?access_token= to the manifest URL (a no-op
when JWT is off), used by the channel-preview panel and the troubleshooting screen.
Mapper.GetPreview drops its iptvJwtEnabled -> Unavailable guard; preview is now
JWT-agnostic.

Live-E2E under JWT: /iptv manifest 401s without a token and passes with a valid one
(garbage token -> 401); token endpoint 401s anonymous, mints with a session.

Honest finding: the issue's point 2 (troubleshooting screen broken under JWT) does
not reproduce -- its live.m3u8 is static-served (UseStaticFiles at /iptv/session),
outside the JWT filter, so it was never gated. The withIptvToken call there is a
harmless defensive no-op.

Docs: security.iptv-browser-token (api-auth-security.md), amended
api.channel-preview-capability, spa-conventions §5b. No OpenAPI change (IgnoreApi +
unchanged Preview schema).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:21:05 +02:00

92 lines
3.0 KiB
C#

using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Application.Channels;
[TestFixture]
public class ChannelPreviewTests
{
[TestCase(StreamingMode.HttpLiveStreamingSegmenter)]
[TestCase(StreamingMode.HttpLiveStreamingDirect)]
public void HlsModes_Are_Available_With_Plain_Manifest_Url(StreamingMode mode)
{
ChannelPreviewResponseModel result =
Mapper.GetPreview(mode, "12.1", isEnabled: true, playoutCount: 1);
result.Availability.ShouldBe("Available");
result.ManifestUrl.ShouldBe("/iptv/channel/12.1.m3u8");
result.UnavailableReason.ShouldBeNull();
}
[TestCase(StreamingMode.TransportStream)]
[TestCase(StreamingMode.TransportStreamHybrid)]
public void TransportStream_Modes_Are_ForcedHlsOnly(StreamingMode mode)
{
ChannelPreviewResponseModel result =
Mapper.GetPreview(mode, "12.1", isEnabled: true, playoutCount: 1);
result.Availability.ShouldBe("ForcedHlsOnly");
result.ManifestUrl.ShouldBe("/iptv/channel/12.1.m3u8?mode=segmenter");
result.UnavailableReason.ShouldBeNull();
}
[Test]
public void Disabled_Channel_Is_Unavailable()
{
ChannelPreviewResponseModel result = Mapper.GetPreview(
StreamingMode.HttpLiveStreamingSegmenter,
"12.1",
isEnabled: false,
playoutCount: 1);
result.Availability.ShouldBe("Unavailable");
result.ManifestUrl.ShouldBeNull();
result.UnavailableReason.ShouldBe("Channel is disabled");
}
[Test]
public void Zero_Playout_Count_Is_Unavailable()
{
ChannelPreviewResponseModel result = Mapper.GetPreview(
StreamingMode.HttpLiveStreamingSegmenter,
"12.1",
isEnabled: true,
playoutCount: 0);
result.Availability.ShouldBe("Unavailable");
result.ManifestUrl.ShouldBeNull();
result.UnavailableReason.ShouldBe("Channel has no playout");
}
[Test]
public void Disabled_Channel_Takes_Precedence_Over_Zero_Playout_Count()
{
// Both causes are true at once: channel disabled (checked first) must win over zero
// playouts (checked second), per the precedence order documented on GetPreview.
ChannelPreviewResponseModel result = Mapper.GetPreview(
StreamingMode.HttpLiveStreamingSegmenter,
"12.1",
isEnabled: false,
playoutCount: 0);
result.Availability.ShouldBe("Unavailable");
result.ManifestUrl.ShouldBeNull();
result.UnavailableReason.ShouldBe("Channel is disabled");
}
[Test]
public void Channel_Number_Is_Used_Verbatim_In_The_Manifest_Url()
{
ChannelPreviewResponseModel result = Mapper.GetPreview(
StreamingMode.HttpLiveStreamingSegmenter,
"7",
isEnabled: true,
playoutCount: 1);
result.ManifestUrl.ShouldBe("/iptv/channel/7.m3u8");
}
}