From fa8bfece4e72c32f8350aaa947c81b2494f6bb77 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 1 Mar 2021 19:35:58 +0100 Subject: Split integration tests from unit tests --- Jellyfin.Server/Properties/AssemblyInfo.cs | 2 +- Jellyfin.sln | 2 + .../Controllers/BrandingControllerTests.cs | 53 --------- .../Controllers/DashboardControllerTests.cs | 86 --------------- tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj | 7 +- .../JellyfinApplicationFactory.cs | 120 --------------------- tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs | 42 -------- tests/Jellyfin.Api.Tests/ParseNetworkTests.cs | 88 --------------- tests/Jellyfin.Api.Tests/TestAppHost.cs | 55 ---------- tests/Jellyfin.Api.Tests/TestPage.html | 9 -- tests/Jellyfin.Api.Tests/TestPlugin.cs | 43 -------- .../Controllers/BrandingControllerTests.cs | 53 +++++++++ .../Controllers/DashboardControllerTests.cs | 86 +++++++++++++++ .../Jellyfin.Server.Integration.Tests.csproj | 40 +++++++ .../JellyfinApplicationFactory.cs | 120 +++++++++++++++++++++ .../OpenApiSpecTests.cs | 42 ++++++++ .../TestAppHost.cs | 55 ++++++++++ .../TestPage.html | 9 ++ .../TestPlugin.cs | 43 ++++++++ .../Jellyfin.Server.Tests.csproj | 39 +++++++ tests/Jellyfin.Server.Tests/ParseNetworkTests.cs | 88 +++++++++++++++ 21 files changed, 580 insertions(+), 502 deletions(-) delete mode 100644 tests/Jellyfin.Api.Tests/Controllers/BrandingControllerTests.cs delete mode 100644 tests/Jellyfin.Api.Tests/Controllers/DashboardControllerTests.cs delete mode 100644 tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs delete mode 100644 tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs delete mode 100644 tests/Jellyfin.Api.Tests/ParseNetworkTests.cs delete mode 100644 tests/Jellyfin.Api.Tests/TestAppHost.cs delete mode 100644 tests/Jellyfin.Api.Tests/TestPage.html delete mode 100644 tests/Jellyfin.Api.Tests/TestPlugin.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj create mode 100644 tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs create mode 100644 tests/Jellyfin.Server.Integration.Tests/TestPage.html create mode 100644 tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs create mode 100644 tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj create mode 100644 tests/Jellyfin.Server.Tests/ParseNetworkTests.cs diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs index 7abf298b1..fe2d5c5f9 100644 --- a/Jellyfin.Server/Properties/AssemblyInfo.cs +++ b/Jellyfin.Server/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ using System.Runtime.InteropServices; // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] -[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")] +[assembly: InternalsVisibleTo("Jellyfin.Server.Tests")] diff --git a/Jellyfin.sln b/Jellyfin.sln index 02ac1c7e9..c749c51e6 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -76,6 +76,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Model.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration.Tests", "tests\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/tests/Jellyfin.Api.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/BrandingControllerTests.cs deleted file mode 100644 index 40933562d..000000000 --- a/tests/Jellyfin.Api.Tests/Controllers/BrandingControllerTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Net.Mime; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using MediaBrowser.Model.Branding; -using Xunit; - -namespace Jellyfin.Api.Tests -{ - public sealed class BrandingControllerTests : IClassFixture - { - private readonly JellyfinApplicationFactory _factory; - - public BrandingControllerTests(JellyfinApplicationFactory factory) - { - _factory = factory; - } - - [Fact] - public async Task GetConfiguration_ReturnsCorrectResponse() - { - // Arrange - var client = _factory.CreateClient(); - - // Act - var response = await client.GetAsync("/Branding/Configuration"); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); - Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var responseBody = await response.Content.ReadAsStreamAsync(); - _ = await JsonSerializer.DeserializeAsync(responseBody); - } - - [Theory] - [InlineData("/Branding/Css")] - [InlineData("/Branding/Css.css")] - public async Task GetCss_ReturnsCorrectResponse(string url) - { - // Arrange - var client = _factory.CreateClient(); - - // Act - var response = await client.GetAsync(url); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("text/css", response.Content.Headers.ContentType?.MediaType); - Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DashboardControllerTests.cs deleted file mode 100644 index 300b2697f..000000000 --- a/tests/Jellyfin.Api.Tests/Controllers/DashboardControllerTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.IO; -using System.Net; -using System.Net.Mime; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Jellyfin.Api.Models; -using MediaBrowser.Common.Json; -using Xunit; - -namespace Jellyfin.Api.Tests.Controllers -{ - public sealed class DashboardControllerTests : IClassFixture - { - private readonly JellyfinApplicationFactory _factory; - private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.GetOptions(); - - public DashboardControllerTests(JellyfinApplicationFactory factory) - { - _factory = factory; - } - - [Fact] - public async Task GetDashboardConfigurationPage_NonExistingPage_NotFound() - { - var client = _factory.CreateClient(); - - var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task GetDashboardConfigurationPage_ExistingPage_CorrectPage() - { - var client = _factory.CreateClient(); - - var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false); - - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType); - StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Api.Tests.TestPage.html")!); - Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd()); - } - - [Fact] - public async Task GetDashboardConfigurationPage_BrokenPage_NotFound() - { - var client = _factory.CreateClient(); - - var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task GetConfigurationPages_NoParams_AllConfigurationPages() - { - var client = _factory.CreateClient(); - - var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false); - - Assert.True(response.IsSuccessStatusCode); - - var res = await response.Content.ReadAsStreamAsync(); - _ = await JsonSerializer.DeserializeAsync(res, _jsonOpions); - // TODO: check content - } - - [Fact] - public async Task GetConfigurationPages_True_MainMenuConfigurationPages() - { - var client = _factory.CreateClient(); - - var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false); - - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); - Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - - var res = await response.Content.ReadAsStreamAsync(); - var data = await JsonSerializer.DeserializeAsync(res, _jsonOpions); - Assert.Empty(data); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index a336d2aee..577b61d02 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -35,11 +35,8 @@ - - - - - + + diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs deleted file mode 100644 index 92c5495c8..000000000 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Threading; -using Emby.Server.Implementations; -using Emby.Server.Implementations.IO; -using Jellyfin.Server; -using MediaBrowser.Common; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Serilog; -using Serilog.Extensions.Logging; - -namespace Jellyfin.Api.Tests -{ - /// - /// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests. - /// - public class JellyfinApplicationFactory : WebApplicationFactory - { - private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); - private readonly ConcurrentBag _disposableComponents = new ConcurrentBag(); - - /// - /// Initializes a new instance of the class. - /// - public JellyfinApplicationFactory() - { - // Perform static initialization that only needs to happen once per test-run - Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); - Program.PerformStaticInitialization(); - } - - /// - protected override IWebHostBuilder CreateWebHostBuilder() - { - return new WebHostBuilder(); - } - - /// - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - // Specify the startup command line options - var commandLineOpts = new StartupOptions - { - NoWebClient = true - }; - - // Use a temporary directory for the application paths - var webHostPathRoot = Path.Combine(_testPathRoot, "test-host-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); - Directory.CreateDirectory(Path.Combine(webHostPathRoot, "logs")); - Directory.CreateDirectory(Path.Combine(webHostPathRoot, "config")); - Directory.CreateDirectory(Path.Combine(webHostPathRoot, "cache")); - Directory.CreateDirectory(Path.Combine(webHostPathRoot, "jellyfin-web")); - var appPaths = new ServerApplicationPaths( - webHostPathRoot, - Path.Combine(webHostPathRoot, "logs"), - Path.Combine(webHostPathRoot, "config"), - Path.Combine(webHostPathRoot, "cache"), - Path.Combine(webHostPathRoot, "jellyfin-web")); - - // Create the logging config file - // TODO: We shouldn't need to do this since we are only logging to console - Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); - - // Create a copy of the application configuration to use for startup - var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); - - ILoggerFactory loggerFactory = new SerilogLoggerFactory(); - var serviceCollection = new ServiceCollection(); - _disposableComponents.Add(loggerFactory); - - // Create the app host and initialize it - var appHost = new TestAppHost( - appPaths, - loggerFactory, - commandLineOpts, - new ConfigurationBuilder().Build(), - new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), - serviceCollection); - _disposableComponents.Add(appHost); - appHost.Init(); - - // Configure the web host builder - Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths); - } - - /// - protected override TestServer CreateServer(IWebHostBuilder builder) - { - // Create the test server using the base implementation - var testServer = base.CreateServer(builder); - - // Finish initializing the app host - var appHost = (TestAppHost)testServer.Services.GetRequiredService(); - appHost.ServiceProvider = testServer.Services; - appHost.InitializeServices().GetAwaiter().GetResult(); - appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); - - return testServer; - } - - /// - protected override void Dispose(bool disposing) - { - foreach (var disposable in _disposableComponents) - { - disposable.Dispose(); - } - - _disposableComponents.Clear(); - - base.Dispose(disposing); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs deleted file mode 100644 index 03ab56d1f..000000000 --- a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.IO; -using System.Reflection; -using System.Text.Json; -using System.Threading.Tasks; -using MediaBrowser.Model.Branding; -using Xunit; -using Xunit.Abstractions; - -namespace Jellyfin.Api.Tests -{ - public sealed class OpenApiSpecTests : IClassFixture - { - private readonly JellyfinApplicationFactory _factory; - private readonly ITestOutputHelper _outputHelper; - - public OpenApiSpecTests(JellyfinApplicationFactory factory, ITestOutputHelper outputHelper) - { - _factory = factory; - _outputHelper = outputHelper; - } - - [Fact] - public async Task GetSpec_ReturnsCorrectResponse() - { - // Arrange - var client = _factory.CreateClient(); - - // Act - var response = await client.GetAsync("/api-docs/openapi.json"); - - // Assert - response.EnsureSuccessStatusCode(); - Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString()); - - // Write out for publishing - var responseBody = await response.Content.ReadAsStringAsync(); - string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); - _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); - File.WriteAllText(outputPath, responseBody); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs deleted file mode 100644 index 3984407ee..000000000 --- a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using Jellyfin.Networking.Configuration; -using Jellyfin.Networking.Manager; -using Jellyfin.Server.Extensions; -using MediaBrowser.Common.Configuration; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests -{ - public class ParseNetworkTests - { - /// - /// Order of the result has always got to be hosts, then networks. - /// - /// IP4 enabled. - /// IP6 enabled. - /// List to parse. - /// What it should match. - [Theory] - // [InlineData(true, true, "192.168.0.0/16,www.yahoo.co.uk", "::ffff:212.82.100.150,::ffff:192.168.0.0/16")] <- fails on Max. www.yahoo.co.uk resolves to a different ip address. - // [InlineData(true, false, "192.168.0.0/16,www.yahoo.co.uk", "212.82.100.150,192.168.0.0/16")] - [InlineData(true, true, "192.168.t,127.0.0.1,1234.1232.12.1234", "::ffff:127.0.0.1")] - [InlineData(true, false, "192.168.x,127.0.0.1,1234.1232.12.1234", "127.0.0.1")] - [InlineData(true, true, "::1", "::1/128")] - public void TestNetworks(bool ip4, bool ip6, string hostList, string match) - { - using var nm = CreateNetworkManager(); - - var settings = new NetworkConfiguration - { - EnableIPV4 = ip4, - EnableIPV6 = ip6 - }; - - var result = match + ","; - ForwardedHeadersOptions options = new ForwardedHeadersOptions(); - - // Need this here as ::1 and 127.0.0.1 are in them by default. - options.KnownProxies.Clear(); - options.KnownNetworks.Clear(); - - ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(','), options); - - var sb = new StringBuilder(); - foreach (var item in options.KnownProxies) - { - sb.Append(item) - .Append(','); - } - - foreach (var item in options.KnownNetworks) - { - sb.Append(item.Prefix) - .Append('/') - .Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture)) - .Append(','); - } - - Assert.Equal(sb.ToString(), result); - } - - private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) - { - var configManager = new Mock - { - CallBase = true - }; - configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf); - return configManager.Object; - } - - private static NetworkManager CreateNetworkManager() - { - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - }; - - return new NetworkManager(GetMockConfig(conf), new NullLogger()); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/TestAppHost.cs b/tests/Jellyfin.Api.Tests/TestAppHost.cs deleted file mode 100644 index eb4c9b305..000000000 --- a/tests/Jellyfin.Api.Tests/TestAppHost.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; -using Emby.Server.Implementations; -using Jellyfin.Server; -using MediaBrowser.Controller; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Api.Tests -{ - /// - /// Implementation of the abstract class. - /// - public class TestAppHost : CoreAppHost - { - /// - /// Initializes a new instance of the class. - /// - /// The to be used by the . - /// The to be used by the . - /// The to be used by the . - /// The to be used by the . - /// The to be used by the . - /// The to be used by the . - public TestAppHost( - IServerApplicationPaths applicationPaths, - ILoggerFactory loggerFactory, - IStartupOptions options, - IConfiguration startup, - IFileSystem fileSystem, - IServiceCollection collection) - : base( - applicationPaths, - loggerFactory, - options, - startup, - fileSystem, - collection) - { - } - - /// - protected override IEnumerable GetAssembliesWithPartsInternal() - { - foreach (var a in base.GetAssembliesWithPartsInternal()) - { - yield return a; - } - - yield return typeof(TestPlugin).Assembly; - } - } -} diff --git a/tests/Jellyfin.Api.Tests/TestPage.html b/tests/Jellyfin.Api.Tests/TestPage.html deleted file mode 100644 index 8037af8a6..000000000 --- a/tests/Jellyfin.Api.Tests/TestPage.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - TestPlugin - - -

This is a Test Page.

- - diff --git a/tests/Jellyfin.Api.Tests/TestPlugin.cs b/tests/Jellyfin.Api.Tests/TestPlugin.cs deleted file mode 100644 index a3b4b6994..000000000 --- a/tests/Jellyfin.Api.Tests/TestPlugin.cs +++ /dev/null @@ -1,43 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Serialization; - -namespace Jellyfin.Api.Tests -{ - public class TestPlugin : BasePlugin, IHasWebPages - { - public TestPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - - public static TestPlugin? Instance { get; private set; } - - public override Guid Id => new Guid("2d350a13-0bf7-4b61-859c-d5e601b5facf"); - - public override string Name => nameof(TestPlugin); - - public override string Description => "Server test Plugin."; - - public IEnumerable GetPages() - { - yield return new PluginPageInfo - { - Name = Name, - EmbeddedResourcePath = GetType().Namespace + ".TestPage.html" - }; - - yield return new PluginPageInfo - { - Name = "BrokenPage", - EmbeddedResourcePath = GetType().Namespace + ".foobar" - }; - } - } -} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs new file mode 100644 index 000000000..fdd93f20e --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs @@ -0,0 +1,53 @@ +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using MediaBrowser.Model.Branding; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests +{ + public sealed class BrandingControllerTests : IClassFixture + { + private readonly JellyfinApplicationFactory _factory; + + public BrandingControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetConfiguration_ReturnsCorrectResponse() + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync("/Branding/Configuration"); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); + Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); + var responseBody = await response.Content.ReadAsStreamAsync(); + _ = await JsonSerializer.DeserializeAsync(responseBody); + } + + [Theory] + [InlineData("/Branding/Css")] + [InlineData("/Branding/Css.css")] + public async Task GetCss_ReturnsCorrectResponse(string url) + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync(url); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("text/css", response.Content.Headers.ContentType?.MediaType); + Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs new file mode 100644 index 000000000..88905fc6d --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Net; +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models; +using MediaBrowser.Common.Json; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + public sealed class DashboardControllerTests : IClassFixture + { + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.GetOptions(); + + public DashboardControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetDashboardConfigurationPage_NonExistingPage_NotFound() + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetDashboardConfigurationPage_ExistingPage_CorrectPage() + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType); + StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!); + Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd()); + } + + [Fact] + public async Task GetDashboardConfigurationPage_BrokenPage_NotFound() + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetConfigurationPages_NoParams_AllConfigurationPages() + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + + var res = await response.Content.ReadAsStreamAsync(); + _ = await JsonSerializer.DeserializeAsync(res, _jsonOpions); + // TODO: check content + } + + [Fact] + public async Task GetConfigurationPages_True_MainMenuConfigurationPages() + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false); + + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); + Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); + + var res = await response.Content.ReadAsStreamAsync(); + var data = await JsonSerializer.DeserializeAsync(res, _jsonOpions); + Assert.Empty(data); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj new file mode 100644 index 000000000..49004966b --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -0,0 +1,40 @@ + + + net5.0 + false + true + enable + AllEnabledByDefault + ../jellyfin-tests.ruleset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs new file mode 100644 index 000000000..94e618102 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using Emby.Server.Implementations; +using Emby.Server.Implementations.IO; +using Jellyfin.Server; +using MediaBrowser.Common; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Extensions.Logging; + +namespace Jellyfin.Server.Integration.Tests +{ + /// + /// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests. + /// + public class JellyfinApplicationFactory : WebApplicationFactory + { + private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); + private readonly ConcurrentBag _disposableComponents = new ConcurrentBag(); + + /// + /// Initializes a new instance of the class. + /// + public JellyfinApplicationFactory() + { + // Perform static initialization that only needs to happen once per test-run + Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); + Program.PerformStaticInitialization(); + } + + /// + protected override IWebHostBuilder CreateWebHostBuilder() + { + return new WebHostBuilder(); + } + + /// + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Specify the startup command line options + var commandLineOpts = new StartupOptions + { + NoWebClient = true + }; + + // Use a temporary directory for the application paths + var webHostPathRoot = Path.Combine(_testPathRoot, "test-host-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "logs")); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "config")); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "cache")); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "jellyfin-web")); + var appPaths = new ServerApplicationPaths( + webHostPathRoot, + Path.Combine(webHostPathRoot, "logs"), + Path.Combine(webHostPathRoot, "config"), + Path.Combine(webHostPathRoot, "cache"), + Path.Combine(webHostPathRoot, "jellyfin-web")); + + // Create the logging config file + // TODO: We shouldn't need to do this since we are only logging to console + Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); + + // Create a copy of the application configuration to use for startup + var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); + + ILoggerFactory loggerFactory = new SerilogLoggerFactory(); + var serviceCollection = new ServiceCollection(); + _disposableComponents.Add(loggerFactory); + + // Create the app host and initialize it + var appHost = new TestAppHost( + appPaths, + loggerFactory, + commandLineOpts, + new ConfigurationBuilder().Build(), + new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), + serviceCollection); + _disposableComponents.Add(appHost); + appHost.Init(); + + // Configure the web host builder + Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths); + } + + /// + protected override TestServer CreateServer(IWebHostBuilder builder) + { + // Create the test server using the base implementation + var testServer = base.CreateServer(builder); + + // Finish initializing the app host + var appHost = (TestAppHost)testServer.Services.GetRequiredService(); + appHost.ServiceProvider = testServer.Services; + appHost.InitializeServices().GetAwaiter().GetResult(); + appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); + + return testServer; + } + + /// + protected override void Dispose(bool disposing) + { + foreach (var disposable in _disposableComponents) + { + disposable.Dispose(); + } + + _disposableComponents.Clear(); + + base.Dispose(disposing); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs new file mode 100644 index 000000000..3cbd638f9 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs @@ -0,0 +1,42 @@ +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using MediaBrowser.Model.Branding; +using Xunit; +using Xunit.Abstractions; + +namespace Jellyfin.Server.Integration.Tests +{ + public sealed class OpenApiSpecTests : IClassFixture + { + private readonly JellyfinApplicationFactory _factory; + private readonly ITestOutputHelper _outputHelper; + + public OpenApiSpecTests(JellyfinApplicationFactory factory, ITestOutputHelper outputHelper) + { + _factory = factory; + _outputHelper = outputHelper; + } + + [Fact] + public async Task GetSpec_ReturnsCorrectResponse() + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync("/api-docs/openapi.json"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString()); + + // Write out for publishing + var responseBody = await response.Content.ReadAsStringAsync(); + string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); + _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); + File.WriteAllText(outputPath, responseBody); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs new file mode 100644 index 000000000..4e5d0fcb6 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Reflection; +using Emby.Server.Implementations; +using Jellyfin.Server; +using MediaBrowser.Controller; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Integration.Tests +{ + /// + /// Implementation of the abstract class. + /// + public class TestAppHost : CoreAppHost + { + /// + /// Initializes a new instance of the class. + /// + /// The to be used by the . + /// The to be used by the . + /// The to be used by the . + /// The to be used by the . + /// The to be used by the . + /// The to be used by the . + public TestAppHost( + IServerApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IStartupOptions options, + IConfiguration startup, + IFileSystem fileSystem, + IServiceCollection collection) + : base( + applicationPaths, + loggerFactory, + options, + startup, + fileSystem, + collection) + { + } + + /// + protected override IEnumerable GetAssembliesWithPartsInternal() + { + foreach (var a in base.GetAssembliesWithPartsInternal()) + { + yield return a; + } + + yield return typeof(TestPlugin).Assembly; + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/TestPage.html b/tests/Jellyfin.Server.Integration.Tests/TestPage.html new file mode 100644 index 000000000..8037af8a6 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/TestPage.html @@ -0,0 +1,9 @@ + + + + TestPlugin + + +

This is a Test Page.

+ + diff --git a/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs b/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs new file mode 100644 index 000000000..1d67ac487 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs @@ -0,0 +1,43 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace Jellyfin.Server.Integration.Tests +{ + public class TestPlugin : BasePlugin, IHasWebPages + { + public TestPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public static TestPlugin? Instance { get; private set; } + + public override Guid Id => new Guid("2d350a13-0bf7-4b61-859c-d5e601b5facf"); + + public override string Name => nameof(TestPlugin); + + public override string Description => "Server test Plugin."; + + public IEnumerable GetPages() + { + yield return new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".TestPage.html" + }; + + yield return new PluginPageInfo + { + Name = "BrokenPage", + EmbeddedResourcePath = GetType().Namespace + ".foobar" + }; + } + } +} diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj new file mode 100644 index 000000000..65ea28e94 --- /dev/null +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -0,0 +1,39 @@ + + + + net5.0 + false + true + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../jellyfin-tests.ruleset + + + diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs new file mode 100644 index 000000000..0b714e80a --- /dev/null +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Globalization; +using System.Text; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using Jellyfin.Server.Extensions; +using MediaBrowser.Common.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Tests +{ + public class ParseNetworkTests + { + /// + /// Order of the result has always got to be hosts, then networks. + /// + /// IP4 enabled. + /// IP6 enabled. + /// List to parse. + /// What it should match. + [Theory] + // [InlineData(true, true, "192.168.0.0/16,www.yahoo.co.uk", "::ffff:212.82.100.150,::ffff:192.168.0.0/16")] <- fails on Max. www.yahoo.co.uk resolves to a different ip address. + // [InlineData(true, false, "192.168.0.0/16,www.yahoo.co.uk", "212.82.100.150,192.168.0.0/16")] + [InlineData(true, true, "192.168.t,127.0.0.1,1234.1232.12.1234", "::ffff:127.0.0.1")] + [InlineData(true, false, "192.168.x,127.0.0.1,1234.1232.12.1234", "127.0.0.1")] + [InlineData(true, true, "::1", "::1/128")] + public void TestNetworks(bool ip4, bool ip6, string hostList, string match) + { + using var nm = CreateNetworkManager(); + + var settings = new NetworkConfiguration + { + EnableIPV4 = ip4, + EnableIPV6 = ip6 + }; + + var result = match + ","; + ForwardedHeadersOptions options = new ForwardedHeadersOptions(); + + // Need this here as ::1 and 127.0.0.1 are in them by default. + options.KnownProxies.Clear(); + options.KnownNetworks.Clear(); + + ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(','), options); + + var sb = new StringBuilder(); + foreach (var item in options.KnownProxies) + { + sb.Append(item) + .Append(','); + } + + foreach (var item in options.KnownNetworks) + { + sb.Append(item.Prefix) + .Append('/') + .Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture)) + .Append(','); + } + + Assert.Equal(sb.ToString(), result); + } + + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf); + return configManager.Object; + } + + private static NetworkManager CreateNetworkManager() + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + return new NetworkManager(GetMockConfig(conf), new NullLogger()); + } + } +} -- cgit v1.2.3