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>
92 lines
3.0 KiB
C#
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");
|
|
}
|
|
}
|