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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
using MediaBrowser.Controller.SyncPlay.Requests;
using MediaBrowser.Model.SyncPlay;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using SyncPlayGroup = Emby.Server.Implementations.SyncPlay.Group;
using SyncPlayManager = Emby.Server.Implementations.SyncPlay.SyncPlayManager;
namespace Jellyfin.Server.Implementations.Tests.SyncPlay;
public class SyncPlayManagerTests
{
[Fact]
public void LeaveGroup_AfterJoiningTheSameGroupTwice_ClearsTheActiveSessionCounter()
{
var harness = new ManagerHarness();
var info = harness.Manager.NewGroup(harness.Session, new NewGroupRequest("group"), CancellationToken.None);
Assert.True(harness.Manager.IsUserActive(harness.User.Id));
// A client that re-sends Join for the group it is already in must not be counted twice.
harness.Manager.JoinGroup(harness.Session, new JoinGroupRequest(info.GroupId), CancellationToken.None);
harness.Manager.LeaveGroup(harness.Session, new LeaveGroupRequest(), CancellationToken.None);
Assert.False(harness.Manager.IsUserActive(harness.User.Id));
}
[Fact]
public void LeaveGroup_AfterASingleJoin_ClearsTheActiveSessionCounter()
{
var harness = new ManagerHarness();
harness.Manager.NewGroup(harness.Session, new NewGroupRequest("group"), CancellationToken.None);
harness.Manager.LeaveGroup(harness.Session, new LeaveGroupRequest(), CancellationToken.None);
Assert.False(harness.Manager.IsUserActive(harness.User.Id));
}
[Fact]
public void IsUserActive_WithTwoSessionsOfTheSameUser_TracksBothSeparately()
{
var harness = new ManagerHarness();
var second = harness.CreateSession("session-2");
var info = harness.Manager.NewGroup(harness.Session, new NewGroupRequest("group"), CancellationToken.None);
harness.Manager.JoinGroup(second, new JoinGroupRequest(info.GroupId), CancellationToken.None);
harness.Manager.LeaveGroup(harness.Session, new LeaveGroupRequest(), CancellationToken.None);
Assert.True(harness.Manager.IsUserActive(harness.User.Id));
harness.Manager.LeaveGroup(second, new LeaveGroupRequest(), CancellationToken.None);
Assert.False(harness.Manager.IsUserActive(harness.User.Id));
}
[Fact]
public async Task HandleRequest_GroupWaitsForAMemberThatNeverReportsReady_RecoversOnItsOwn()
{
var harness = new ManagerHarness(groupWaitTimeout: 200);
var second = harness.CreateSession("session-2");
var info = harness.Manager.NewGroup(harness.Session, new NewGroupRequest("group"), CancellationToken.None);
harness.Manager.JoinGroup(second, new JoinGroupRequest(info.GroupId), CancellationToken.None);
// Starting playback puts the group behind the ready barrier.
harness.Manager.HandleRequest(
harness.Session,
new PlayGroupRequest(new[] { Guid.NewGuid() }, 0, 0),
CancellationToken.None);
Assert.Equal(GroupStateType.Waiting, harness.Manager.GetGroup(harness.Session, info.GroupId).State);
// Neither session ever reports ready, so the group has to come out of the wait by itself.
Assert.Equal(
GroupStateType.Playing,
await harness.WaitForState(harness.Session, info.GroupId, GroupStateType.Playing));
}
private sealed class ManagerHarness
{
private readonly Mock<ISessionManager> _sessionManager = new();
public ManagerHarness(long? groupWaitTimeout = null)
{
var userManager = new Mock<IUserManager>();
var libraryManager = new Mock<ILibraryManager>();
User = new User("tester", "auth-provider", "pwdreset-provider");
userManager.Setup(m => m.GetUserById(It.IsAny<Guid>())).Returns(User);
var item = new Mock<BaseItem>();
item.Setup(i => i.IsVisibleStandalone(It.IsAny<User>())).Returns(true);
item.Object.RunTimeTicks = TimeSpan.FromHours(2).Ticks;
libraryManager.Setup(m => m.GetItemById(It.IsAny<Guid>())).Returns(item.Object);
_sessionManager
.Setup(m => m.SendSyncPlayCommand(It.IsAny<string>(), It.IsAny<SendCommand>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
_sessionManager
.Setup(m => m.SendSyncPlayGroupUpdate(It.IsAny<string>(), It.IsAny<GroupUpdate<GroupStateUpdate>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
Manager = new SyncPlayManager(
NullLoggerFactory.Instance,
userManager.Object,
_sessionManager.Object,
libraryManager.Object)
{
GroupWaitTimeout = groupWaitTimeout ?? SyncPlayGroup.DefaultGroupWaitTimeout
};
Session = CreateSession("session-1");
}
public SyncPlayManager Manager { get; }
public User User { get; }
public SessionInfo Session { get; }
public SessionInfo CreateSession(string id)
{
return new SessionInfo(_sessionManager.Object, NullLogger.Instance)
{
Id = id,
UserId = User.Id,
UserName = User.Username
};
}
public async Task<GroupStateType> WaitForState(SessionInfo session, Guid groupId, GroupStateType expected)
{
var deadline = DateTime.UtcNow.AddSeconds(10);
GroupStateType state;
while ((state = Manager.GetGroup(session, groupId).State) != expected && DateTime.UtcNow < deadline)
{
await Task.Delay(20, TestContext.Current.CancellationToken);
}
return state;
}
}
}
|