mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-10-23 03:27:39 +09:00
Backport pull request #18026 from jellyfin/release-12.z
Fix device access revocation not logging out existing sessions
Original-merge: 950c45cf20
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Cody Robibero <cody@robibe.ro>
This commit is contained in:
1 parent
57d19b185c
commit
c74ddebd17
6 files changed
+191
-5
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -17,6 +18,7 @@ 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;
|
||||
@@ -119,6 +121,27 @@ public sealed class UserManagerUpdateUserTests : IDisposable
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Integration.Tests;
|
||||
|
||||
public sealed class HostedServiceRegistrationTests : IClassFixture<JellyfinApplicationFactory>
|
||||
{
|
||||
private readonly JellyfinApplicationFactory _factory;
|
||||
|
||||
public HostedServiceRegistrationTests(JellyfinApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeviceAccessHost_IsRegisteredAsHostedService()
|
||||
{
|
||||
_ = _factory.CreateClient();
|
||||
|
||||
var hostedServices = _factory.Services.GetServices<IHostedService>();
|
||||
|
||||
Assert.Contains(hostedServices, service => service is DeviceAccessHost);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user