Add Live TV published URL regression coverage
This commit is contained in:
@@ -230,6 +230,7 @@ namespace Jellyfin.Api.Tests.Helpers
|
||||
[InlineData(MediaProtocol.Http, "http://192.168.1.50:5004/live/channel1.ts")]
|
||||
[InlineData(MediaProtocol.File, "/media/livetv/buffer/abc/stream.ts")]
|
||||
[InlineData(MediaProtocol.Http, "http://172.19.0.3:8096/Videos/abc/stream.ts")]
|
||||
[InlineData(MediaProtocol.Http, "http://172.19.0.3:8096/library/movie.strm")]
|
||||
public async Task OpenMediaSource_NotAPublishableLiveStreamFilesPath_PathUnchanged(MediaProtocol protocol, string path)
|
||||
{
|
||||
var mediaSource = new MediaSourceInfo
|
||||
@@ -283,6 +284,136 @@ namespace Jellyfin.Api.Tests.Helpers
|
||||
Assert.Equal(LocalPath, response.MediaSource.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenMediaSource_ExplicitPortOverrideWithBaseUrl_RewritesToOverrideHostAndPort()
|
||||
{
|
||||
// Mirrors NetworkManager.GetBindAddress resolving a "internal=myhost:8097" override: the smart API
|
||||
// URL carries an explicit non-default port alongside the configured BaseUrl.
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Id = "abc",
|
||||
Protocol = MediaProtocol.Http,
|
||||
Path = "http://172.19.0.3:8096/jellyfin" + LiveStreamFilesPath,
|
||||
LiveStreamId = "livestream-1"
|
||||
};
|
||||
|
||||
var helper = CreateOpenMediaSourceHelper(mediaSource, "http://myhost:8097/jellyfin", "/jellyfin");
|
||||
|
||||
var response = await helper.OpenMediaSource(new DefaultHttpContext(), new LiveStreamRequest()).ConfigureAwait(true);
|
||||
|
||||
Assert.Equal("http://myhost:8097/jellyfin" + LiveStreamFilesPath, response.MediaSource.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlaybackInfo_TwoRequestsForSharedLiveStream_ReceiveIndependentSmartApiBases()
|
||||
{
|
||||
const string LocalPath = "http://172.19.0.3:8096" + LiveStreamFilesPath;
|
||||
|
||||
// Both requests resolve the same live stream; the manager hands back its own shared instance each time.
|
||||
var sharedLiveSource = new MediaSourceInfo
|
||||
{
|
||||
Id = "abc",
|
||||
Protocol = MediaProtocol.Http,
|
||||
Path = LocalPath,
|
||||
LiveStreamId = "livestream-1"
|
||||
};
|
||||
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
mediaSourceManager
|
||||
.Setup(x => x.GetLiveStream(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(sharedLiveSource);
|
||||
|
||||
var requestA = new DefaultHttpContext().Request;
|
||||
var requestB = new DefaultHttpContext().Request;
|
||||
|
||||
var appHost = new Mock<IServerApplicationHost>();
|
||||
appHost.Setup(x => x.GetSmartApiUrl(requestA)).Returns("https://a.example.com");
|
||||
appHost.Setup(x => x.GetSmartApiUrl(requestB)).Returns("https://b.example.com");
|
||||
|
||||
var helper = CreateHelper(mediaSourceManager: mediaSourceManager.Object, appHost: appHost.Object);
|
||||
|
||||
var resultA = await helper.GetPlaybackInfo(new Movie(), null, requestA, liveStreamId: "live-1").ConfigureAwait(true);
|
||||
var resultB = await helper.GetPlaybackInfo(new Movie(), null, requestB, liveStreamId: "live-1").ConfigureAwait(true);
|
||||
|
||||
Assert.Equal("https://a.example.com" + LiveStreamFilesPath, resultA.MediaSources[0].Path);
|
||||
Assert.Equal("https://b.example.com" + LiveStreamFilesPath, resultB.MediaSources[0].Path);
|
||||
|
||||
// Neither request's rewrite may leak into the other's response or into the shared instance.
|
||||
Assert.NotEqual(resultA.MediaSources[0].Path, resultB.MediaSources[0].Path);
|
||||
Assert.Equal(LocalPath, sharedLiveSource.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlaybackInfo_AutoOpenLiveStreamFlow_MergedOpenedSourceHasRewrittenPath()
|
||||
{
|
||||
// Reproduces MediaInfoController.GetPostedPlaybackInfo's AutoOpenLiveStream branch (~line 220-246):
|
||||
// it picks the RequiresOpening source out of GetPlaybackInfo's result, calls OpenMediaSource, then
|
||||
// merges by replacing result.MediaSources with the opened source. Building a full controller fixture
|
||||
// is impractical (it pulls in many unrelated dependencies), so this test drives the same two helper
|
||||
// calls the controller makes and asserts the merged source is the rewritten one.
|
||||
var itemId = Guid.NewGuid();
|
||||
var sourceId = itemId.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
// The pre-open placeholder source carries a different local path than the one OpenMediaSource
|
||||
// eventually returns, so the final assertion can prove the merge picked up the freshly opened
|
||||
// source rather than the stale placeholder.
|
||||
var requiresOpeningSource = new MediaSourceInfo
|
||||
{
|
||||
Id = sourceId,
|
||||
Protocol = MediaProtocol.Http,
|
||||
Path = "http://172.19.0.3:8096/LiveTv/LiveStreamFiles/placeholder/stream.ts",
|
||||
RequiresOpening = true,
|
||||
LiveStreamId = string.Empty
|
||||
};
|
||||
|
||||
var openedSource = new MediaSourceInfo
|
||||
{
|
||||
Id = sourceId,
|
||||
Protocol = MediaProtocol.Http,
|
||||
Path = "http://172.19.0.3:8096" + LiveStreamFilesPath,
|
||||
LiveStreamId = "livestream-1"
|
||||
};
|
||||
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
mediaSourceManager
|
||||
.Setup(x => x.GetPlaybackMediaSources(It.IsAny<BaseItem>(), It.IsAny<User>(), true, true, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new[] { requiresOpeningSource });
|
||||
mediaSourceManager
|
||||
.Setup(x => x.OpenLiveStream(It.IsAny<LiveStreamRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() =>
|
||||
{
|
||||
// MediaSourceManager.OpenLiveStream JSON-clones its internal MediaSourceInfo before returning
|
||||
// it (see Emby.Server.Implementations/Library/MediaSourceManager.cs:693-706); mirror that so
|
||||
// the in-place rewrite below can't be observed on openedSource itself.
|
||||
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(JsonSerializer.SerializeToUtf8Bytes(openedSource))!;
|
||||
return new LiveStreamResponse(clone);
|
||||
});
|
||||
|
||||
var appHost = new Mock<IServerApplicationHost>();
|
||||
appHost.Setup(x => x.GetSmartApiUrl(It.IsAny<HttpRequest>())).Returns("https://media.example.com");
|
||||
|
||||
var helper = CreateHelper(mediaSourceManager: mediaSourceManager.Object, appHost: appHost.Object);
|
||||
|
||||
var info = await helper.GetPlaybackInfo(new Movie(), null, Mock.Of<HttpRequest>()).ConfigureAwait(true);
|
||||
|
||||
var mediaSource = info.MediaSources[0];
|
||||
Assert.True(mediaSource.RequiresOpening);
|
||||
var preOpenPath = mediaSource.Path;
|
||||
|
||||
var openStreamResult = await helper.OpenMediaSource(
|
||||
new DefaultHttpContext(),
|
||||
new LiveStreamRequest { OpenToken = mediaSource.OpenToken, ItemId = itemId }).ConfigureAwait(true);
|
||||
|
||||
// MediaInfoController.cs:245 - info.MediaSources = new[] { openStreamResult.MediaSource };
|
||||
info.MediaSources = new[] { openStreamResult.MediaSource };
|
||||
|
||||
Assert.Equal("https://media.example.com" + LiveStreamFilesPath, info.MediaSources[0].Path);
|
||||
Assert.NotEqual(preOpenPath, info.MediaSources[0].Path);
|
||||
|
||||
// The pristine OpenLiveStream response object must remain unrewritten; only the merged clone changed.
|
||||
Assert.Equal("http://172.19.0.3:8096" + LiveStreamFilesPath, openedSource.Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(
|
||||
"https://media.example.com",
|
||||
|
||||
@@ -6,6 +6,7 @@ using Jellyfin.Networking.Manager;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -507,6 +508,10 @@ namespace Jellyfin.Networking.Tests
|
||||
[InlineData("8.8.8.8", "all=https://secure.jellyfin.org:8920", "https://secure.jellyfin.org:8920", null)]
|
||||
// Hostname beginning with "http" is a hostname, not a URL scheme.
|
||||
[InlineData("8.8.8.8", "all=http-proxy.lan:8097", "http-proxy.lan", 8097)]
|
||||
// Literal "internal" keyword override (applies to every LAN subnet) with an explicit port.
|
||||
[InlineData("192.168.1.1", "internal=myhost.internal:8097", "myhost.internal", 8097)]
|
||||
// Literal "external" keyword override with an explicit port.
|
||||
[InlineData("8.8.8.8", "external=myhost.external:9090", "myhost.external", 9090)]
|
||||
public void GetBindAddress_PublishedServerOverride_ParsesHostAndPort(string source, string publishedServers, string expectedHost, int? expectedPort)
|
||||
{
|
||||
var conf = new NetworkConfiguration
|
||||
@@ -527,5 +532,92 @@ namespace Jellyfin.Networking.Tests
|
||||
Assert.Equal(expectedHost, intf);
|
||||
Assert.Equal(expectedPort, port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression coverage for <c>IServerApplicationHost.GetApiUrlForLocalAccess()</c>, which calls
|
||||
/// <see cref="NetworkManager.GetBindAddress(IPAddress, out int?, bool)"/> with a null source address.
|
||||
/// Published server URL overrides are only matched when a source address is supplied
|
||||
/// (<c>MatchesPublishedServerUrl</c> requires it), so a null source must never come back as a published
|
||||
/// CLI/dashboard URL - it must fall back to a plain local bind address.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetBindAddress_NullSource_DoesNotApplyPublishedServerOverride()
|
||||
{
|
||||
var conf = new NetworkConfiguration
|
||||
{
|
||||
LocalNetworkSubnets = new[] { "192.168.1.0/24" },
|
||||
LocalNetworkAddresses = new[] { "eth16", "eth11" },
|
||||
EnableIPv4 = true,
|
||||
PublishedServerUriBySubnet = new[] { "all=http://published.example.com" }
|
||||
};
|
||||
|
||||
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
||||
var startupConf = new Mock<IConfiguration>();
|
||||
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||
NetworkManager.MockNetworkSettings = string.Empty;
|
||||
|
||||
var result = nm.GetBindAddress((IPAddress?)null, out var port);
|
||||
|
||||
Assert.Equal("192.168.1.208", result);
|
||||
Assert.Null(port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="NetworkManager.GetBindAddress(HttpRequest, out int?)"/> is the piece of request-host
|
||||
/// normalization that a request-host-aware smart API URL policy relies on: it resolves the bind address
|
||||
/// from the request's host and falls back to the request's own port when no override applies.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetBindAddress_HttpRequestOverload_FallsBackToRequestPortWhenNoOverride()
|
||||
{
|
||||
var conf = new NetworkConfiguration
|
||||
{
|
||||
LocalNetworkSubnets = new[] { "192.168.1.0/24" },
|
||||
LocalNetworkAddresses = new[] { "eth16", "eth11" },
|
||||
EnableIPv4 = true
|
||||
};
|
||||
|
||||
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
||||
var startupConf = new Mock<IConfiguration>();
|
||||
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||
NetworkManager.MockNetworkSettings = string.Empty;
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Host = new HostString("192.168.1.1", 34567);
|
||||
|
||||
var result = nm.GetBindAddress(httpContext.Request, out var port);
|
||||
|
||||
Assert.Equal("192.168.1.208", result);
|
||||
Assert.Equal(34567, port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ordering check: a dashboard published-server-URL override's explicit port takes precedence over the
|
||||
/// request's own port, even though the request's host chose which override subnet matched.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetBindAddress_HttpRequestOverload_PublishedOverridePortWinsOverRequestPort()
|
||||
{
|
||||
var conf = new NetworkConfiguration
|
||||
{
|
||||
LocalNetworkSubnets = new[] { "192.168.1.0/24" },
|
||||
LocalNetworkAddresses = new[] { "eth16", "eth11" },
|
||||
EnableIPv4 = true,
|
||||
PublishedServerUriBySubnet = new[] { "internal=myhost.internal:9000" }
|
||||
};
|
||||
|
||||
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
|
||||
var startupConf = new Mock<IConfiguration>();
|
||||
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
|
||||
NetworkManager.MockNetworkSettings = string.Empty;
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Host = new HostString("192.168.1.1", 34567);
|
||||
|
||||
var result = nm.GetBindAddress(httpContext.Request, out var port);
|
||||
|
||||
Assert.Equal("myhost.internal", result);
|
||||
Assert.Equal(9000, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user