aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Implementations.Tests/Users
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Users')
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/DeviceAccessHostTests.cs111
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerConcurrentCreateTests.cs77
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerFailedWriteTests.cs99
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerTests.cs53
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerUpdateUserTests.cs204
6 files changed, 546 insertions, 0 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/DeviceAccessHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/DeviceAccessHostTests.cs
new file mode 100644
index 0000000000..5bb5081b60
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/DeviceAccessHostTests.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Entities.Security;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Querying;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users;
+
+public class DeviceAccessHostTests
+{
+ [Fact]
+ public async Task OnUserUpdated_LogoutThrows_DoesNotEscapeToThreadPool()
+ {
+ var user = new User("test", "default", "default");
+ var device = new Device(user.Id, "app", "1.0", "device", "device-id");
+
+ var deviceManager = new Mock<IDeviceManager>();
+ deviceManager.Setup(d => d.GetDevices(It.IsAny<DeviceQuery>()))
+ .Returns(new QueryResult<Device>(new[] { device }));
+ deviceManager.Setup(d => d.CanAccessDevice(user, device.DeviceId)).Returns(false);
+
+ var sessionManager = new Mock<ISessionManager>();
+ sessionManager.Setup(s => s.Logout(It.IsAny<Device>()))
+ .ThrowsAsync(new ObjectDisposedException(nameof(ISessionManager)));
+
+ var userManager = new Mock<IUserManager>();
+ var host = new DeviceAccessHost(
+ userManager.Object,
+ deviceManager.Object,
+ sessionManager.Object,
+ NullLogger<DeviceAccessHost>.Instance);
+ await host.StartAsync(TestContext.Current.CancellationToken);
+
+ var context = new CapturingSynchronizationContext();
+ var previous = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(context);
+ try
+ {
+ userManager.Raise(m => m.OnUserUpdated += null, userManager.Object, new GenericEventArgs<User>(user));
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(previous);
+ }
+
+ Assert.Empty(context.Exceptions);
+ }
+
+ [Fact]
+ public async Task OnUserUpdated_DeviceNoLongerAllowed_LogsOutDevice()
+ {
+ var user = new User("test", "default", "default");
+ var device = new Device(user.Id, "app", "1.0", "device", "device-id");
+
+ var deviceManager = new Mock<IDeviceManager>();
+ deviceManager.Setup(d => d.GetDevices(It.IsAny<DeviceQuery>()))
+ .Returns(new QueryResult<Device>(new[] { device }));
+ deviceManager.Setup(d => d.CanAccessDevice(user, device.DeviceId)).Returns(false);
+
+ var loggedOut = new TaskCompletionSource();
+ var sessionManager = new Mock<ISessionManager>();
+ sessionManager.Setup(s => s.Logout(It.IsAny<Device>()))
+ .Callback(() => loggedOut.TrySetResult())
+ .Returns(Task.CompletedTask);
+
+ var userManager = new Mock<IUserManager>();
+ var host = new DeviceAccessHost(
+ userManager.Object,
+ deviceManager.Object,
+ sessionManager.Object,
+ NullLogger<DeviceAccessHost>.Instance);
+ await host.StartAsync(TestContext.Current.CancellationToken);
+
+ userManager.Raise(m => m.OnUserUpdated += null, userManager.Object, new GenericEventArgs<User>(user));
+
+ await loggedOut.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
+ sessionManager.Verify(s => s.Logout(device), Times.Once);
+ }
+
+ private sealed class CapturingSynchronizationContext : SynchronizationContext
+ {
+ public List<Exception> Exceptions { get; } = new List<Exception>();
+
+ public override void Post(SendOrPostCallback d, object? state) => Run(d, state);
+
+ public override void Send(SendOrPostCallback d, object? state) => Run(d, state);
+
+ private void Run(SendOrPostCallback d, object? state)
+ {
+ try
+ {
+ d(state);
+ }
+ catch (Exception ex)
+ {
+ Exceptions.Add(ex);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerConcurrentCreateTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerConcurrentCreateTests.cs
new file mode 100644
index 0000000000..72ca680cf2
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerConcurrentCreateTests.cs
@@ -0,0 +1,77 @@
+using System;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.Implementations.Tests.Item;
+using Jellyfin.Server.Implementations.Users;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users;
+
+/// <summary>
+/// Two first-ever requests for the same preferences can both find nothing and both insert; the unique index lets one
+/// through. The other has to return the stored preferences instead of failing.
+/// </summary>
+public sealed class DisplayPreferencesManagerConcurrentCreateTests : SqliteDbTestFixture
+{
+ private const string Client = "client";
+
+ private static readonly Guid _userId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ private static readonly Guid _itemId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+
+ private readonly CompetingInsert _competingInsert;
+ private readonly DisplayPreferencesManager _manager;
+
+ public DisplayPreferencesManagerConcurrentCreateTests()
+ : this(new CompetingInsert())
+ {
+ }
+
+ private DisplayPreferencesManagerConcurrentCreateTests(CompetingInsert competingInsert)
+ : base(competingInsert)
+ {
+ _competingInsert = competingInsert;
+ _manager = new DisplayPreferencesManager(CreateDbContextFactory());
+
+ using var context = CreateDbContext();
+ context.Users.Add(new User("user", "auth-provider", "reset-provider") { Id = _userId });
+ context.SaveChanges();
+ }
+
+ [Fact]
+ public void GetDisplayPreferences_AnotherRequestStoresThemFirst_ReturnsTheStoredPreferences()
+ {
+ var storedId = 0;
+ _competingInsert.Before = () =>
+ {
+ using var context = CreateDbContext();
+ var stored = new DisplayPreferences(_userId, _itemId, Client);
+ context.DisplayPreferences.Add(stored);
+ context.SaveChanges();
+ storedId = stored.Id;
+ };
+
+ var preferences = _manager.GetDisplayPreferences(_userId, _itemId, Client);
+
+ Assert.NotEqual(0, storedId);
+ Assert.Equal(storedId, preferences.Id);
+ using var check = CreateDbContext();
+ Assert.Single(check.DisplayPreferences);
+ }
+
+ /// <summary>
+ /// Stores the other request's row just before the next save, after the manager has looked for the preferences.
+ /// </summary>
+ private sealed class CompetingInsert : SaveChangesInterceptor
+ {
+ public Action? Before { get; set; }
+
+ public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
+ {
+ // Disarmed first, because the competing save goes through this interceptor as well.
+ var before = Before;
+ Before = null;
+ before?.Invoke();
+ return result;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerFailedWriteTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerFailedWriteTests.cs
new file mode 100644
index 0000000000..13688c7574
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerFailedWriteTests.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using Jellyfin.Server.Implementations.Tests.Item;
+using Jellyfin.Server.Implementations.Users;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users;
+
+public sealed class DisplayPreferencesManagerFailedWriteTests : SqliteDbTestFixture
+{
+ private const string Client = "client";
+
+ private static readonly Guid _userId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ private static readonly Guid _itemId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+
+ private readonly StatementInterceptor _statements;
+ private readonly DisplayPreferencesManager _manager;
+
+ public DisplayPreferencesManagerFailedWriteTests()
+ : this(new StatementInterceptor())
+ {
+ }
+
+ private DisplayPreferencesManagerFailedWriteTests(StatementInterceptor statements)
+ : base(statements)
+ {
+ _statements = statements;
+ _manager = new DisplayPreferencesManager(CreateDbContextFactory());
+ }
+
+ [Fact]
+ public void SetCustomItemDisplayPreferences_InsertFails_KeepsThePreviousPreferences()
+ {
+ _manager.SetCustomItemDisplayPreferences(_userId, _itemId, Client, new Dictionary<string, string?> { ["first"] = "1" });
+
+ _statements.FailInserts = true;
+ Assert.Throws<DbUpdateException>(() => _manager.SetCustomItemDisplayPreferences(_userId, _itemId, Client, new Dictionary<string, string?> { ["second"] = "2" }));
+
+ Assert.Equal(
+ new Dictionary<string, string?> { ["first"] = "1" },
+ _manager.ListCustomItemDisplayPreferences(_userId, _itemId, Client));
+ }
+
+ [Fact]
+ public void SetCustomItemDisplayPreferences_AnotherRequestStoredAKeyAfterTheDelete_StoresThePreferencesItWasGiven()
+ {
+ _manager.SetCustomItemDisplayPreferences(_userId, _itemId, Client, new Dictionary<string, string?> { ["first"] = "1" });
+
+ // The delete misses the row, as it does when another request stores it after this one's delete, so the unique
+ // index rejects this request's insert.
+ _statements.SkipNextDelete = true;
+ _manager.SetCustomItemDisplayPreferences(_userId, _itemId, Client, new Dictionary<string, string?> { ["first"] = "2" });
+
+ Assert.Equal(
+ new Dictionary<string, string?> { ["first"] = "2" },
+ _manager.ListCustomItemDisplayPreferences(_userId, _itemId, Client));
+ }
+
+ /// <summary>
+ /// Fails inserts into CustomItemDisplayPreferences with the error SQLite reports when the unique index rejects a row, or
+ /// leaves out the next delete from it.
+ /// </summary>
+ private sealed class StatementInterceptor : DbCommandInterceptor
+ {
+ public bool FailInserts { get; set; }
+
+ public bool SkipNextDelete { get; set; }
+
+ public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
+ {
+ FailInsert(command);
+ return result;
+ }
+
+ public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
+ {
+ if (SkipNextDelete && command.CommandText.StartsWith("DELETE FROM \"CustomItemDisplayPreferences\"", StringComparison.Ordinal))
+ {
+ SkipNextDelete = false;
+ return InterceptionResult<int>.SuppressWithResult(0);
+ }
+
+ FailInsert(command);
+ return result;
+ }
+
+ private void FailInsert(DbCommand command)
+ {
+ if (FailInserts && command.CommandText.StartsWith("INSERT INTO \"CustomItemDisplayPreferences\"", StringComparison.Ordinal))
+ {
+ throw new SqliteException("SQLite Error 19: 'UNIQUE constraint failed: CustomItemDisplayPreferences.UserId, CustomItemDisplayPreferences.ItemId, CustomItemDisplayPreferences.Client, CustomItemDisplayPreferences.Key'.", 19);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerTests.cs
new file mode 100644
index 0000000000..4110eb513f
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/DisplayPreferencesManagerTests.cs
@@ -0,0 +1,53 @@
+using System;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.Implementations.Tests.Item;
+using Jellyfin.Server.Implementations.Users;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users;
+
+/// <summary>
+/// Item display preferences are looked up by the item they belong to, so they have to be stored under it as well:
+/// a row stored under any other item is never found again, and every later request stores another one.
+/// </summary>
+public sealed class DisplayPreferencesManagerTests : SqliteDbTestFixture
+{
+ private const string Client = "client";
+
+ private static readonly Guid _userId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ private static readonly Guid _itemId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+
+ private readonly DisplayPreferencesManager _manager;
+
+ public DisplayPreferencesManagerTests()
+ {
+ _manager = new DisplayPreferencesManager(CreateDbContextFactory());
+
+ using var context = CreateDbContext();
+ context.Users.Add(new User("user", "auth-provider", "reset-provider") { Id = _userId });
+ context.SaveChanges();
+ }
+
+ [Fact]
+ public void GetItemDisplayPreferences_NewItem_StoresThePreferencesUnderThatItem()
+ {
+ var preferences = _manager.GetItemDisplayPreferences(_userId, _itemId, Client);
+
+ Assert.Equal(_itemId, preferences.ItemId);
+
+ using var context = CreateDbContext();
+ Assert.Equal(_itemId, Assert.Single(context.ItemDisplayPreferences).ItemId);
+ }
+
+ [Fact]
+ public void GetItemDisplayPreferences_SameItemTwice_ReturnsTheStoredPreferences()
+ {
+ var first = _manager.GetItemDisplayPreferences(_userId, _itemId, Client);
+ var second = _manager.GetItemDisplayPreferences(_userId, _itemId, Client);
+
+ Assert.Equal(first.Id, second.Id);
+
+ using var context = CreateDbContext();
+ Assert.Single(context.ItemDisplayPreferences);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
index 4cea53bd3d..2bf1d1d05b 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Tests.Users
[InlineData(" thishasaspaceatthestart")]
[InlineData(" thishasaspaceatbothends ")]
[InlineData(" this has a space at both ends and inbetween ")]
+ [InlineData(".")]
+ [InlineData("..")]
public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username)
{
Assert.Throws<ArgumentException>(() => UserManager.ThrowIfInvalidUsername(username));
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerUpdateUserTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerUpdateUserTests.cs
new file mode 100644
index 0000000000..e91ebdf1b6
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerUpdateUserTests.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Locking;
+using Jellyfin.Database.Providers.Sqlite;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.Users;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users;
+
+public sealed class UserManagerUpdateUserTests : IDisposable
+{
+ private readonly SqliteConnection _connection;
+ private readonly DbContextOptions<JellyfinDbContext> _dbOptions;
+ private readonly UserManager _userManager;
+
+ public UserManagerUpdateUserTests()
+ {
+ _connection = new SqliteConnection("Data Source=:memory:");
+ _connection.Open();
+
+ _dbOptions = new DbContextOptionsBuilder<JellyfinDbContext>()
+ .UseSqlite(_connection)
+ .Options;
+
+ // Create the schema
+ using var ctx = CreateDbContext();
+ ctx.Database.EnsureCreated();
+
+ var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
+ factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
+ factory.Setup(f => f.CreateDbContextAsync(It.IsAny<CancellationToken>()))
+ .ReturnsAsync(CreateDbContext);
+
+ var cryptoProvider = new Mock<ICryptoProvider>();
+ var configManager = new Mock<IServerConfigurationManager>();
+ var appPaths = new Mock<IServerApplicationPaths>();
+ appPaths.Setup(x => x.ProgramDataPath).Returns(Path.GetTempPath());
+ configManager.Setup(x => x.ApplicationPaths).Returns(appPaths.Object);
+
+ var appHost = new Mock<IApplicationHost>();
+
+ var defaultAuthProvider = new DefaultAuthenticationProvider(
+ NullLogger<DefaultAuthenticationProvider>.Instance,
+ cryptoProvider.Object);
+ var invalidAuthProvider = new InvalidAuthProvider();
+ var defaultPasswordResetProvider = new DefaultPasswordResetProvider(
+ configManager.Object,
+ appHost.Object);
+
+ _userManager = new UserManager(
+ factory.Object,
+ new NoopEventManager(),
+ new Mock<INetworkManager>().Object,
+ appHost.Object,
+ new Mock<IImageProcessor>().Object,
+ NullLogger<UserManager>.Instance,
+ configManager.Object,
+ [defaultPasswordResetProvider],
+ [defaultAuthProvider, invalidAuthProvider]);
+ }
+
+ public void Dispose()
+ {
+ _userManager.Dispose();
+ _connection.Dispose();
+ }
+
+ [Fact]
+ public async Task UpdateUserAsync_DoesNotDetachPermissionsOrPreferences()
+ {
+ var user = await _userManager.CreateUserAsync("orphanuser");
+ var permissionCount = user.Permissions.Count;
+ var preferenceCount = user.Preferences.Count;
+
+ user.LastActivityDate = DateTime.UtcNow;
+ await _userManager.UpdateUserAsync(user);
+ await _userManager.UpdateUserAsync(user);
+
+ await using var context = CreateDbContext();
+ Assert.Equal(permissionCount, await context.Permissions.CountAsync(TestContext.Current.CancellationToken));
+ Assert.Equal(preferenceCount, await context.Preferences.CountAsync(TestContext.Current.CancellationToken));
+ Assert.All(
+ await context.Permissions.ToListAsync(TestContext.Current.CancellationToken),
+ permission => Assert.Equal(user.Id, permission.UserId));
+ Assert.All(
+ await context.Preferences.ToListAsync(TestContext.Current.CancellationToken),
+ preference => Assert.Equal(user.Id, preference.UserId));
+ }
+
+ [Fact]
+ public async Task UpdateUserAsync_WhenOnlyTheUserRowChanged_LeavesChildRowsUntouched()
+ {
+ var user = await _userManager.CreateUserAsync("churnuser");
+ var before = await ReadChildRowsAsync();
+
+ // A session activity stamp goes through the same path. It must not rewrite all 37 child
+ // rows, which is what tearing the collections down and rebuilding them used to do.
+ user.LastActivityDate = DateTime.UtcNow;
+ await _userManager.UpdateUserAsync(user);
+
+ Assert.Equal(before, await ReadChildRowsAsync());
+ }
+
+ [Fact]
+ public async Task UpdatePolicyAsync_RaisesOnUserUpdated()
+ {
+ var user = await _userManager.CreateUserAsync("policyeventuser");
+
+ User? updated = null;
+ _userManager.OnUserUpdated += (_, e) => updated = e.Argument;
+
+ await _userManager.UpdatePolicyAsync(
+ user.Id,
+ new UserPolicy
+ {
+ EnableAllDevices = false,
+ AuthenticationProviderId = user.AuthenticationProviderId,
+ PasswordResetProviderId = user.PasswordResetProviderId
+ });
+
+ Assert.NotNull(updated);
+ Assert.Equal(user.Id, updated.Id);
+ }
+
+ [Fact]
+ public async Task UpdateUserAsync_AppliesPermissionAndPreferenceChanges()
+ {
+ var user = await _userManager.CreateUserAsync("policyuser");
+ Assert.False(user.HasPermission(PermissionKind.IsAdministrator));
+
+ user.SetPermission(PermissionKind.IsAdministrator, true);
+ user.SetPreference(PreferenceKind.BlockedTags, ["spoilers"]);
+ user.Permissions.Remove(user.Permissions.First(permission => permission.Kind == PermissionKind.EnableAllChannels));
+
+ await _userManager.UpdateUserAsync(user);
+
+ var reloaded = _userManager.GetUserById(user.Id)!;
+ Assert.True(reloaded.HasPermission(PermissionKind.IsAdministrator));
+ Assert.Equal(new[] { "spoilers" }, reloaded.GetPreference(PreferenceKind.BlockedTags));
+ Assert.DoesNotContain(reloaded.Permissions, permission => permission.Kind == PermissionKind.EnableAllChannels);
+
+ await using var context = CreateDbContext();
+ Assert.Equal(reloaded.Permissions.Count, await context.Permissions.CountAsync(TestContext.Current.CancellationToken));
+ }
+
+ private JellyfinDbContext CreateDbContext()
+ {
+ return new JellyfinDbContext(
+ _dbOptions,
+ NullLogger<JellyfinDbContext>.Instance,
+ new SqliteDatabaseProvider(null!, NullLogger<SqliteDatabaseProvider>.Instance),
+ new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
+ }
+
+ /// <summary>
+ /// Reads the identity and concurrency token of every permission and preference row.
+ /// </summary>
+ private async Task<List<(string Table, int Id, int Kind, uint RowVersion)>> ReadChildRowsAsync()
+ {
+ await using var context = CreateDbContext();
+ var permissions = await context.Permissions
+ .OrderBy(permission => permission.Id)
+ .Select(permission => new ValueTuple<string, int, int, uint>("Permission", permission.Id, (int)permission.Kind, permission.RowVersion))
+ .ToListAsync(TestContext.Current.CancellationToken);
+ var preferences = await context.Preferences
+ .OrderBy(preference => preference.Id)
+ .Select(preference => new ValueTuple<string, int, int, uint>("Preference", preference.Id, (int)preference.Kind, preference.RowVersion))
+ .ToListAsync(TestContext.Current.CancellationToken);
+
+ return permissions.Concat(preferences).ToList();
+ }
+
+ private sealed class NoopEventManager : IEventManager
+ {
+ public void Publish<T>(T eventArgs)
+ where T : EventArgs
+ {
+ }
+
+ public Task PublishAsync<T>(T eventArgs)
+ where T : EventArgs
+ => Task.CompletedTask;
+ }
+}