1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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);
}
|