aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Integration.Tests/StartOverTests.cs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Jellyfin.Server.Integration.Tests/StartOverTests.cs')
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/StartOverTests.cs119
1 files changed, 119 insertions, 0 deletions
diff --git a/tests/Jellyfin.Server.Integration.Tests/StartOverTests.cs b/tests/Jellyfin.Server.Integration.Tests/StartOverTests.cs
new file mode 100644
index 0000000000..b8c149891f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/StartOverTests.cs
@@ -0,0 +1,119 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Serialization;
+using Jellyfin.Api.Models.StartupDtos;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests;
+
+/// <summary>
+/// Covers starting a server that has been set up before after its database has been deleted.
+/// </summary>
+public sealed class StartOverTests : IDisposable
+{
+ private readonly string _root = Path.Combine(Path.GetTempPath(), "jellyfin-test-data", "start-over-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+
+ [Fact]
+ public async Task DeletedDatabase_WizardReset_StartsAsNewServer()
+ {
+ var paths = await SetUpServerAndDeleteDatabaseAsync();
+ SetWizardCompleted(paths, false);
+
+ using var factory = new SameRootApplicationFactory(_root);
+ using var client = factory.CreateClient();
+
+ using var response = await client.GetAsync("/Startup/Configuration", TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var dbContextFactory = factory.Services.GetRequiredService<IDbContextFactory<JellyfinDbContext>>();
+ await using var context = await dbContextFactory.CreateDbContextAsync(TestContext.Current.CancellationToken);
+ var applied = (await context.Database.GetAppliedMigrationsAsync(TestContext.Current.CancellationToken)).ToHashSet(StringComparer.Ordinal);
+ Assert.All(context.GetService<IMigrationsAssembly>().Migrations.Keys, id => Assert.Contains(id, applied));
+ }
+
+ [Fact]
+ public async Task DeletedDatabase_WizardStillCompleted_RefusesToStart()
+ {
+ var paths = await SetUpServerAndDeleteDatabaseAsync();
+ var systemConfiguration = await File.ReadAllBytesAsync(paths.SystemConfigurationFilePath, TestContext.Current.CancellationToken);
+
+ using var factory = new SameRootApplicationFactory(_root);
+ var exception = Assert.ThrowsAny<Exception>(() => factory.CreateClient());
+
+ var guard = exception as InvalidOperationException ?? exception.InnerException as InvalidOperationException;
+ Assert.NotNull(guard);
+ Assert.Contains("the database does not exist", guard.Message, StringComparison.Ordinal);
+ Assert.Empty(Directory.GetFiles(paths.DataPath, "jellyfin.db*"));
+ Assert.Equal(systemConfiguration, await File.ReadAllBytesAsync(paths.SystemConfigurationFilePath, TestContext.Current.CancellationToken));
+ }
+
+ public void Dispose()
+ {
+ SqliteConnection.ClearAllPools();
+ try
+ {
+ Directory.Delete(_root, true);
+ }
+ catch (IOException)
+ {
+ // Best effort, a locked file must not fail the test.
+ }
+ }
+
+ private static void SetWizardCompleted(IApplicationPaths paths, bool completed)
+ {
+ var serializer = new MyXmlSerializer();
+ var configuration = (ServerConfiguration)serializer.DeserializeFromFile(typeof(ServerConfiguration), paths.SystemConfigurationFilePath)!;
+ configuration.IsStartupWizardCompleted = completed;
+ serializer.SerializeToFile(configuration, paths.SystemConfigurationFilePath);
+ }
+
+ private async Task<IApplicationPaths> SetUpServerAndDeleteDatabaseAsync()
+ {
+ IApplicationPaths paths;
+ using (var factory = new SameRootApplicationFactory(_root))
+ {
+ using var client = factory.CreateClient();
+ paths = factory.Services.GetRequiredService<IApplicationPaths>();
+
+ using (var response = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ var user = new StartupUserDto { Name = "StartOver", Password = "StartOver" };
+ using (var response = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions, TestContext.Current.CancellationToken))
+ {
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ }
+ }
+
+ // Completing the wizard through the API would queue a library scan that can still hold the database file when
+ // it is deleted below, so the flag is set on the stopped server instead.
+ SetWizardCompleted(paths, true);
+ SqliteConnection.ClearAllPools();
+ foreach (var file in Directory.GetFiles(paths.DataPath, "jellyfin.db*"))
+ {
+ File.Delete(file);
+ }
+
+ return paths;
+ }
+
+ private sealed class SameRootApplicationFactory(string webHostPathRoot) : JellyfinApplicationFactory(webHostPathRoot);
+}