Skip to content

Commit 0962663

Browse files
fix(config): Update scheduled task trigger on config change (#840)
1 parent 2c4da1d commit 0962663

5 files changed

Lines changed: 386 additions & 10 deletions

File tree

Jellyfin.Plugin.Themerr.Tests/TestThemerrStartupService.cs

Lines changed: 267 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
1+
using System.Reflection;
12
using System.Threading;
3+
using Jellyfin.Plugin.Themerr.Configuration;
24
using MediaBrowser.Common.Configuration;
35
using MediaBrowser.Controller.Library;
6+
using MediaBrowser.Model.Plugins;
7+
using MediaBrowser.Model.Serialization;
8+
using MediaBrowser.Model.Tasks;
49
using Microsoft.Extensions.Logging;
510
using Moq;
611

712
namespace Jellyfin.Plugin.Themerr.Tests;
813

14+
[Collection("Fixture Collection")]
915
public class TestThemerrStartupService
1016
{
1117
[Fact]
1218
[Trait("Category", "Unit")]
1319
public async Task TestStartAndStop()
1420
{
21+
ClearThemerrPluginInstance();
22+
1523
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
1624
Mock<ILibraryManager> mockLibraryManager = new();
1725
Mock<ILoggerFactory> mockLoggerFactory = new();
26+
Mock<ITaskManager> mockTaskManager = new();
1827

1928
mockLoggerFactory
2029
.Setup(x => x.CreateLogger(It.IsAny<string>()))
2130
.Returns(new Mock<ILogger>().Object);
31+
mockTaskManager.Setup(x => x.ScheduledTasks).Returns(Array.Empty<IScheduledTaskWorker>());
2232

2333
var service = new ThemerrStartupService(
2434
mockApplicationPaths.Object,
2535
mockLibraryManager.Object,
26-
mockLoggerFactory.Object);
36+
mockLoggerFactory.Object,
37+
mockTaskManager.Object);
2738

2839
var startTask = service.StartAsync(CancellationToken.None);
2940
Assert.True(startTask.IsCompletedSuccessfully);
@@ -33,4 +44,259 @@ public async Task TestStartAndStop()
3344
Assert.True(stopTask.IsCompletedSuccessfully);
3445
await stopTask;
3546
}
47+
48+
[Fact]
49+
[Trait("Category", "Unit")]
50+
public async Task TestConfigurationChangeUpdatesScheduledTaskTrigger()
51+
{
52+
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
53+
Mock<ILibraryManager> mockLibraryManager = new();
54+
Mock<ILoggerFactory> mockLoggerFactory = new();
55+
Mock<IXmlSerializer> mockXmlSerializer = new();
56+
Mock<ITaskManager> mockTaskManager = new();
57+
Mock<IScheduledTaskWorker> mockTaskWorker = new();
58+
59+
mockLoggerFactory
60+
.Setup(x => x.CreateLogger(It.IsAny<string>()))
61+
.Returns(new Mock<ILogger>().Object);
62+
63+
_ = new ThemerrPlugin(mockApplicationPaths.Object, mockXmlSerializer.Object);
64+
ThemerrPlugin.Instance.UpdateConfiguration(new Configuration.PluginConfiguration());
65+
66+
var themerrTasks = new ScheduledTasks.ThemerrTasks(
67+
mockApplicationPaths.Object,
68+
mockLibraryManager.Object,
69+
Mock.Of<ILogger<ScheduledTasks.ThemerrTasks>>(),
70+
mockLoggerFactory.Object);
71+
72+
mockTaskWorker.Setup(x => x.ScheduledTask).Returns(themerrTasks);
73+
mockTaskWorker.SetupProperty(x => x.Triggers, Array.Empty<TaskTriggerInfo>());
74+
mockTaskManager.Setup(x => x.ScheduledTasks).Returns(new[] { mockTaskWorker.Object });
75+
76+
var service = new ThemerrStartupService(
77+
mockApplicationPaths.Object,
78+
mockLibraryManager.Object,
79+
mockLoggerFactory.Object,
80+
mockTaskManager.Object);
81+
82+
await service.StartAsync(CancellationToken.None);
83+
84+
var configuration = new Configuration.PluginConfiguration
85+
{
86+
UpdateInterval = 30,
87+
};
88+
ThemerrPlugin.Instance.UpdateConfiguration(configuration);
89+
90+
var trigger = Assert.Single(mockTaskWorker.Object.Triggers);
91+
Assert.Equal(TimeSpan.FromMinutes(30).Ticks, trigger.IntervalTicks);
92+
mockTaskWorker.Verify(x => x.ReloadTriggerEvents(), Times.Exactly(2));
93+
94+
await service.StopAsync(CancellationToken.None);
95+
96+
configuration = new Configuration.PluginConfiguration
97+
{
98+
UpdateInterval = 45,
99+
};
100+
ThemerrPlugin.Instance.UpdateConfiguration(configuration);
101+
102+
mockTaskWorker.Verify(x => x.ReloadTriggerEvents(), Times.Exactly(2));
103+
}
104+
105+
[Fact]
106+
[Trait("Category", "Unit")]
107+
public async Task TestConfigurationChangePreservesUserScheduledTaskTriggers()
108+
{
109+
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
110+
Mock<ILibraryManager> mockLibraryManager = new();
111+
Mock<ILoggerFactory> mockLoggerFactory = new();
112+
Mock<IXmlSerializer> mockXmlSerializer = new();
113+
Mock<ITaskManager> mockTaskManager = new();
114+
Mock<IScheduledTaskWorker> mockTaskWorker = new();
115+
116+
mockLoggerFactory
117+
.Setup(x => x.CreateLogger(It.IsAny<string>()))
118+
.Returns(new Mock<ILogger>().Object);
119+
120+
_ = new ThemerrPlugin(mockApplicationPaths.Object, mockXmlSerializer.Object);
121+
ThemerrPlugin.Instance.UpdateConfiguration(new Configuration.PluginConfiguration
122+
{
123+
UpdateInterval = 45,
124+
});
125+
126+
var themerrTasks = new ScheduledTasks.ThemerrTasks(
127+
mockApplicationPaths.Object,
128+
mockLibraryManager.Object,
129+
Mock.Of<ILogger<ScheduledTasks.ThemerrTasks>>(),
130+
mockLoggerFactory.Object);
131+
132+
var userDailyTrigger = new TaskTriggerInfo
133+
{
134+
Type = TaskTriggerInfoType.DailyTrigger,
135+
TimeOfDayTicks = TimeSpan.FromHours(1).Ticks,
136+
};
137+
var userStartupTrigger = new TaskTriggerInfo
138+
{
139+
Type = TaskTriggerInfoType.StartupTrigger,
140+
};
141+
142+
mockTaskWorker.Setup(x => x.ScheduledTask).Returns(themerrTasks);
143+
mockTaskWorker.SetupProperty(
144+
x => x.Triggers,
145+
new[]
146+
{
147+
new TaskTriggerInfo
148+
{
149+
Type = TaskTriggerInfoType.IntervalTrigger,
150+
IntervalTicks = TimeSpan.FromMinutes(15).Ticks,
151+
},
152+
userDailyTrigger,
153+
userStartupTrigger,
154+
});
155+
mockTaskManager.Setup(x => x.ScheduledTasks).Returns(new[] { mockTaskWorker.Object });
156+
157+
var service = new ThemerrStartupService(
158+
mockApplicationPaths.Object,
159+
mockLibraryManager.Object,
160+
mockLoggerFactory.Object,
161+
mockTaskManager.Object);
162+
163+
await service.StartAsync(CancellationToken.None);
164+
165+
Assert.Collection(
166+
mockTaskWorker.Object.Triggers,
167+
trigger =>
168+
{
169+
Assert.Equal(TaskTriggerInfoType.IntervalTrigger, trigger.Type);
170+
Assert.Equal(TimeSpan.FromMinutes(45).Ticks, trigger.IntervalTicks);
171+
},
172+
trigger => Assert.Same(userDailyTrigger, trigger),
173+
trigger => Assert.Same(userStartupTrigger, trigger));
174+
175+
ThemerrPlugin.Instance.UpdateConfiguration(new Configuration.PluginConfiguration
176+
{
177+
UpdateInterval = 30,
178+
});
179+
180+
Assert.Collection(
181+
mockTaskWorker.Object.Triggers,
182+
trigger =>
183+
{
184+
Assert.Equal(TaskTriggerInfoType.IntervalTrigger, trigger.Type);
185+
Assert.Equal(TimeSpan.FromMinutes(30).Ticks, trigger.IntervalTicks);
186+
},
187+
trigger => Assert.Same(userDailyTrigger, trigger),
188+
trigger => Assert.Same(userStartupTrigger, trigger));
189+
mockTaskWorker.Verify(x => x.ReloadTriggerEvents(), Times.Exactly(2));
190+
191+
await service.StopAsync(CancellationToken.None);
192+
}
193+
194+
[Fact]
195+
[Trait("Category", "Unit")]
196+
public async Task TestStartWithPluginWithoutConfigurationDoesNotUpdateScheduledTaskTrigger()
197+
{
198+
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
199+
Mock<ILibraryManager> mockLibraryManager = new();
200+
Mock<ILoggerFactory> mockLoggerFactory = new();
201+
Mock<IXmlSerializer> mockXmlSerializer = new();
202+
Mock<ITaskManager> mockTaskManager = new();
203+
Mock<IScheduledTaskWorker> mockTaskWorker = new();
204+
205+
mockLoggerFactory
206+
.Setup(x => x.CreateLogger(It.IsAny<string>()))
207+
.Returns(new Mock<ILogger>().Object);
208+
209+
_ = new ThemerrPlugin(mockApplicationPaths.Object, mockXmlSerializer.Object);
210+
mockTaskManager.Setup(x => x.ScheduledTasks).Returns(new[] { mockTaskWorker.Object });
211+
212+
var service = new ThemerrStartupService(
213+
mockApplicationPaths.Object,
214+
mockLibraryManager.Object,
215+
mockLoggerFactory.Object,
216+
mockTaskManager.Object);
217+
218+
await service.StartAsync(CancellationToken.None);
219+
220+
mockTaskWorker.VerifySet(x => x.Triggers = It.IsAny<IReadOnlyList<TaskTriggerInfo>>(), Times.Never);
221+
mockTaskWorker.Verify(x => x.ReloadTriggerEvents(), Times.Never);
222+
223+
await service.StopAsync(CancellationToken.None);
224+
}
225+
226+
[Fact]
227+
[Trait("Category", "Unit")]
228+
public async Task TestConfigurationChangeIgnoresNonPluginConfiguration()
229+
{
230+
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
231+
Mock<ILibraryManager> mockLibraryManager = new();
232+
Mock<ILoggerFactory> mockLoggerFactory = new();
233+
Mock<IXmlSerializer> mockXmlSerializer = new();
234+
Mock<ITaskManager> mockTaskManager = new();
235+
Mock<IScheduledTaskWorker> mockTaskWorker = new();
236+
237+
mockLoggerFactory
238+
.Setup(x => x.CreateLogger(It.IsAny<string>()))
239+
.Returns(new Mock<ILogger>().Object);
240+
241+
_ = new ThemerrPlugin(mockApplicationPaths.Object, mockXmlSerializer.Object);
242+
ThemerrPlugin.Instance.UpdateConfiguration(new PluginConfiguration());
243+
mockTaskManager.Setup(x => x.ScheduledTasks).Returns(Array.Empty<IScheduledTaskWorker>());
244+
245+
var service = new ThemerrStartupService(
246+
mockApplicationPaths.Object,
247+
mockLibraryManager.Object,
248+
mockLoggerFactory.Object,
249+
mockTaskManager.Object);
250+
251+
await service.StartAsync(CancellationToken.None);
252+
253+
ThemerrPlugin.Instance.ConfigurationChanged?.Invoke(this, new BasePluginConfiguration());
254+
255+
mockTaskWorker.VerifySet(x => x.Triggers = It.IsAny<IReadOnlyList<TaskTriggerInfo>>(), Times.Never);
256+
mockTaskWorker.Verify(x => x.ReloadTriggerEvents(), Times.Never);
257+
258+
await service.StopAsync(CancellationToken.None);
259+
}
260+
261+
[Fact]
262+
[Trait("Category", "Unit")]
263+
public async Task TestConfigurationChangeWithoutThemerrTaskDoesNotUpdateScheduledTaskTrigger()
264+
{
265+
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
266+
Mock<ILibraryManager> mockLibraryManager = new();
267+
Mock<ILoggerFactory> mockLoggerFactory = new();
268+
Mock<IXmlSerializer> mockXmlSerializer = new();
269+
Mock<ITaskManager> mockTaskManager = new();
270+
Mock<IScheduledTaskWorker> mockTaskWorker = new();
271+
272+
mockLoggerFactory
273+
.Setup(x => x.CreateLogger(It.IsAny<string>()))
274+
.Returns(new Mock<ILogger>().Object);
275+
276+
_ = new ThemerrPlugin(mockApplicationPaths.Object, mockXmlSerializer.Object);
277+
ThemerrPlugin.Instance.UpdateConfiguration(new PluginConfiguration());
278+
mockTaskManager.Setup(x => x.ScheduledTasks).Returns(new[] { mockTaskWorker.Object });
279+
280+
var service = new ThemerrStartupService(
281+
mockApplicationPaths.Object,
282+
mockLibraryManager.Object,
283+
mockLoggerFactory.Object,
284+
mockTaskManager.Object);
285+
286+
await service.StartAsync(CancellationToken.None);
287+
ThemerrPlugin.Instance.UpdateConfiguration(new PluginConfiguration { UpdateInterval = 30 });
288+
289+
mockTaskWorker.VerifySet(x => x.Triggers = It.IsAny<IReadOnlyList<TaskTriggerInfo>>(), Times.Never);
290+
mockTaskWorker.Verify(x => x.ReloadTriggerEvents(), Times.Never);
291+
292+
await service.StopAsync(CancellationToken.None);
293+
}
294+
295+
private static void ClearThemerrPluginInstance()
296+
{
297+
typeof(ThemerrPlugin)
298+
.GetProperty(nameof(ThemerrPlugin.Instance), BindingFlags.Public | BindingFlags.Static)
299+
?.GetSetMethod(nonPublic: true)
300+
?.Invoke(null, new object?[] { null });
301+
}
36302
}

Jellyfin.Plugin.Themerr.Tests/TestThemerrTasks.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
using Jellyfin.Plugin.Themerr.Configuration;
12
using Jellyfin.Plugin.Themerr.ScheduledTasks;
23
using MediaBrowser.Common.Configuration;
34
using MediaBrowser.Controller.Library;
5+
using MediaBrowser.Model.Serialization;
6+
using MediaBrowser.Model.Tasks;
47
using Microsoft.Extensions.Logging;
58
using Moq;
69

710
namespace Jellyfin.Plugin.Themerr.Tests;
811

12+
[Collection("Fixture Collection")]
913
public class TestThemerrTasks
1014
{
1115
[Fact]
@@ -32,4 +36,43 @@ public void TestConstructorAndProperties()
3236
Assert.Equal("Scans all libraries to download supported Theme Songs", tasks.Description);
3337
Assert.Equal("Themerr", tasks.Category);
3438
}
39+
40+
[Fact]
41+
[Trait("Category", "Unit")]
42+
public void TestGetTriggersUsesConfiguredInterval()
43+
{
44+
var trigger = Assert.Single(ThemerrTasks.GetTriggers(30));
45+
46+
Assert.Equal(TaskTriggerInfoType.IntervalTrigger, trigger.Type);
47+
Assert.Equal(TimeSpan.FromMinutes(30).Ticks, trigger.IntervalTicks);
48+
}
49+
50+
[Fact]
51+
[Trait("Category", "Unit")]
52+
public void TestGetDefaultTriggersUsesPluginConfiguration()
53+
{
54+
Mock<IApplicationPaths> mockApplicationPaths = TestHelper.GetMockApplicationPaths();
55+
Mock<ILibraryManager> mockLibraryManager = new();
56+
Mock<ILogger<ThemerrTasks>> mockLogger = new();
57+
Mock<ILoggerFactory> mockLoggerFactory = new();
58+
Mock<IXmlSerializer> mockXmlSerializer = new();
59+
60+
mockLoggerFactory
61+
.Setup(x => x.CreateLogger(It.IsAny<string>()))
62+
.Returns(new Mock<ILogger>().Object);
63+
64+
_ = new ThemerrPlugin(mockApplicationPaths.Object, mockXmlSerializer.Object);
65+
ThemerrPlugin.Instance.UpdateConfiguration(new PluginConfiguration { UpdateInterval = 45 });
66+
67+
var tasks = new ThemerrTasks(
68+
mockApplicationPaths.Object,
69+
mockLibraryManager.Object,
70+
mockLogger.Object,
71+
mockLoggerFactory.Object);
72+
73+
var trigger = Assert.Single(tasks.GetDefaultTriggers());
74+
75+
Assert.Equal(TaskTriggerInfoType.IntervalTrigger, trigger.Type);
76+
Assert.Equal(TimeSpan.FromMinutes(45).Ticks, trigger.IntervalTicks);
77+
}
3578
}

Jellyfin.Plugin.Themerr/Configuration/PluginConfiguration.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ namespace Jellyfin.Plugin.Themerr.Configuration
77
/// </summary>
88
public class PluginConfiguration : BasePluginConfiguration
99
{
10+
/// <summary>
11+
/// The minimum interval between scheduled updates, in minutes.
12+
/// </summary>
13+
public const int MinimumUpdateIntervalMinutes = 15;
14+
1015
private int _updateInterval;
1116

1217
/// <summary>
@@ -32,8 +37,7 @@ public int UpdateInterval
3237
{
3338
get => _updateInterval;
3439

35-
// todo - modify the existing scheduled task
36-
set => _updateInterval = value < 15 ? 15 : value;
40+
set => _updateInterval = value < MinimumUpdateIntervalMinutes ? MinimumUpdateIntervalMinutes : value;
3741
}
3842
}
3943
}

0 commit comments

Comments
 (0)