aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Tests/Migrations/CodeMigrationTests.cs
blob: 3bd8581a5f135aafa714de09c714d9917d7227b6 (plain)
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
using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Server.Migrations;
using Jellyfin.Server.Migrations.Stages;
using Jellyfin.Server.ServerSetupApp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Jellyfin.Server.Tests.Migrations;

public class CodeMigrationTests
{
    [Fact]
    public async Task Perform_LeavesApplicationSingletonsAlive()
    {
        var services = new ServiceCollection()
            .AddLogging()
            .RegisterStartupLogger()
            .AddSingleton<ApplicationSingleton>()
            .AddTransient<MigrationTransient>();

        await using var serviceProvider = services.BuildServiceProvider();
        var applicationSingleton = serviceProvider.GetRequiredService<ApplicationSingleton>();
        var logger = new StartupLogger(NullLogger.Instance).BeginGroup($"Test migration");

        var migration = new CodeMigration(
            typeof(TestMigration),
            new JellyfinMigrationAttribute("2026-09-05T10:00:00", nameof(TestMigration)),
            null);
        await migration.Perform(serviceProvider, logger, CancellationToken.None);

        var performed = TestMigration.Performed;
        Assert.NotNull(performed);
        // The migration has to run against the applications own services, and they have to outlive it.
        Assert.Same(applicationSingleton, performed.Singleton);
        Assert.False(applicationSingleton.IsDisposed);
        Assert.Same(applicationSingleton, serviceProvider.GetRequiredService<ApplicationSingleton>());
        // Services created for the migration itself are still owned by the migration.
        Assert.True(performed.Transient.IsDisposed);
        // The startup logger has to stay attached to the topic of the running migration.
        Assert.Same(logger.Topic, performed.Logger.Topic);
    }

    [Fact]
    public async Task Perform_DoesNotLeakTheMigrationTopic()
    {
        var services = new ServiceCollection()
            .AddLogging()
            .RegisterStartupLogger()
            .AddSingleton<ApplicationSingleton>()
            .AddTransient<MigrationTransient>();

        await using var serviceProvider = services.BuildServiceProvider();
        var logger = new StartupLogger(NullLogger.Instance).BeginGroup($"Test migration");

        var migration = new CodeMigration(
            typeof(TestMigration),
            new JellyfinMigrationAttribute("2026-09-05T10:00:00", nameof(TestMigration)),
            null);
        await migration.Perform(serviceProvider, logger, CancellationToken.None);

        // The topic belongs to the migration that ran, so loggers resolved afterwards must not still write into it.
        Assert.Null(serviceProvider.GetRequiredService<IStartupLogger<CodeMigrationTests>>().Topic);
        Assert.Null(new StartupLogger(NullLogger.Instance).Topic);
    }

    private sealed class ApplicationSingleton : IDisposable
    {
        public bool IsDisposed { get; private set; }

        public void Dispose()
        {
            IsDisposed = true;
        }
    }

    private sealed class MigrationTransient : IDisposable
    {
        public bool IsDisposed { get; private set; }

        public void Dispose()
        {
            IsDisposed = true;
        }
    }

    private sealed class TestMigration : IAsyncMigrationRoutine
    {
        public TestMigration(ApplicationSingleton singleton, MigrationTransient transient, IStartupLogger<TestMigration> logger)
        {
            Singleton = singleton;
            Transient = transient;
            Logger = logger;
        }

        public static TestMigration? Performed { get; private set; }

        public ApplicationSingleton Singleton { get; }

        public MigrationTransient Transient { get; }

        public IStartupLogger<TestMigration> Logger { get; }

        public Task PerformAsync(CancellationToken cancellationToken)
        {
            Performed = this;
            return Task.CompletedTask;
        }
    }
}