diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 1c84622ac0..daac9b0a2e 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
MinimumVisualStudioVersion = 10.0.40219.1
@@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementat
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs
new file mode 100644
index 0000000000..0bd9909f5b
--- /dev/null
+++ b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Emby.Server.Implementations;
+using Emby.Server.Implementations.IO;
+using Emby.Server.Implementations.Networking;
+using Jellyfin.Drawing.Skia;
+using Jellyfin.Server;
+using MediaBrowser.Common;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Extensions.Logging;
+
+namespace MediaBrowser.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 static readonly ConcurrentBag _appHosts = new ConcurrentBag();
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ 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,
+ NoAutoRunWebApp = 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).Wait();
+
+ // Create a copy of the application configuration to use for startup
+ var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
+
+ // Create the app host and initialize it
+ ILoggerFactory loggerFactory = new SerilogLoggerFactory();
+ var appHost = new CoreAppHost(
+ appPaths,
+ loggerFactory,
+ commandLineOpts,
+ new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths),
+ new SkiaEncoder(loggerFactory.CreateLogger(), appPaths),
+ new NetworkManager(loggerFactory.CreateLogger()));
+ _appHosts.Add(appHost);
+ var serviceCollection = new ServiceCollection();
+ appHost.InitAsync(serviceCollection, startupConfig).Wait();
+
+ // 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 = (CoreAppHost)testServer.Services.GetRequiredService();
+ appHost.ServiceProvider = testServer.Services;
+ appHost.InitializeServices();
+ appHost.FindParts();
+ appHost.RunStartupTasksAsync().Wait();
+
+ return testServer;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ foreach (var host in _appHosts)
+ {
+ host.Dispose();
+ }
+
+ _appHosts.Clear();
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
new file mode 100644
index 0000000000..313cf9a3d7
--- /dev/null
+++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netcoreapp3.1
+ false
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+