aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Implementations.Tests/Users/DeviceAccessHostTests.cs
blob: 5bb5081b60c25564eeb2882357f62daab222236d (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
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);
            }
        }
    }
}