diff options
Diffstat (limited to 'tests/Jellyfin.Api.Tests/Controllers')
3 files changed, 223 insertions, 0 deletions
diff --git a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs index 1f06e8fde6..5f5f273f12 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs @@ -1,5 +1,9 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Controllers; +using MediaBrowser.Controller.MediaEncoding; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Jellyfin.Api.Tests.Controllers @@ -41,5 +45,77 @@ namespace Jellyfin.Api.Tests.Controllers return data; } + + [Fact] + public async Task WaitForActiveTranscodingRequests_WaitsUntilRequestCompletes() + { + var job = new TranscodingJob(NullLogger<TranscodingJob>.Instance) + { + ActiveRequestCount = 1 + }; + + var waitTask = DynamicHlsController.WaitForActiveTranscodingRequests(job, CancellationToken.None); + Assert.False(waitTask.IsCompleted); + + job.DecrementActiveRequestCount(); + + await waitTask; + } + + [Fact] + public async Task WaitForActiveTranscodingRequests_WaitsForEveryRequest() + { + var job = new TranscodingJob(NullLogger<TranscodingJob>.Instance) + { + ActiveRequestCount = 2 + }; + + var waitTask = DynamicHlsController.WaitForActiveTranscodingRequests(job, CancellationToken.None); + job.DecrementActiveRequestCount(); + + await Task.Delay(150, TestContext.Current.CancellationToken); + Assert.False(waitTask.IsCompleted); + + job.DecrementActiveRequestCount(); + + await waitTask; + } + + [Fact] + public async Task WaitForActiveTranscodingRequests_ReturnsWithoutAnActiveRequest() + { + var job = new TranscodingJob(NullLogger<TranscodingJob>.Instance); + + await DynamicHlsController.WaitForActiveTranscodingRequests(job, CancellationToken.None); + await DynamicHlsController.WaitForActiveTranscodingRequests(null, CancellationToken.None); + } + + [Fact] + public async Task WaitForActiveTranscodingRequests_ObservesCancellation() + { + var job = new TranscodingJob(NullLogger<TranscodingJob>.Instance) + { + ActiveRequestCount = 1 + }; + using var cancellationTokenSource = new CancellationTokenSource(); + + var waitTask = DynamicHlsController.WaitForActiveTranscodingRequests(job, cancellationTokenSource.Token); + await cancellationTokenSource.CancelAsync(); + + await Assert.ThrowsAnyAsync<OperationCanceledException>(() => waitTask); + } + + [Fact] + public async Task ActiveRequestCount_UpdatesAtomically() + { + const int RequestCount = 1000; + var job = new TranscodingJob(NullLogger<TranscodingJob>.Instance); + + await Task.WhenAll( + Task.Run(() => Parallel.For(0, RequestCount, _ => job.IncrementActiveRequestCount())), + Task.Run(() => Parallel.For(0, RequestCount, _ => job.DecrementActiveRequestCount()))); + + Assert.Equal(0, job.ActiveRequestCount); + } } } diff --git a/tests/Jellyfin.Api.Tests/Controllers/ItemUpdateControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ItemUpdateControllerTests.cs new file mode 100644 index 0000000000..1a91efe4f2 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/ItemUpdateControllerTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading.Tasks; +using Jellyfin.Api.Controllers; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers; + +public class ItemUpdateControllerTests +{ + private readonly ItemUpdateController _subject; + + public ItemUpdateControllerTests() + { + _subject = new ItemUpdateController( + Mock.Of<IFileSystem>(), + Mock.Of<ILibraryManager>(), + Mock.Of<IProviderManager>(), + Mock.Of<ILocalizationManager>(), + Mock.Of<IServerConfigurationManager>()); + } + + [Fact] + public async Task UpdateItem_WhenOnlyTagsFieldSupplied_DoesNotThrowAndAppliesTags() + { + // Regression test for https://github.com/jellyfin/jellyfin/issues/17366 + // A partial update payload that only sets "Tags" leaves every other + // BaseItemDto collection property null (they have no default + // initializer). Genres and ProviderIds used to be fed straight into + // Distinct()/ToList() without a null check, so this call used to throw + // ArgumentNullException before the fix below was applied. + var movie = new Movie(); + var request = new BaseItemDto + { + Tags = new[] { "new-tag-1", "new-tag-2" } + }; + + await InvokeUpdateItem(request, movie); + + Assert.Equal(new[] { "new-tag-1", "new-tag-2" }, movie.Tags); + Assert.Empty(movie.Genres); + Assert.Empty(movie.ProviderIds); + } + + [Fact] + public async Task UpdateItem_WhenGenresAndProviderIdsOmitted_LeavesExistingValuesUnchanged() + { + var movie = new Movie + { + Genres = new[] { "Action" } + }; + movie.ProviderIds["Imdb"] = "tt1234567"; + + var request = new BaseItemDto + { + Tags = Array.Empty<string>() + }; + + await InvokeUpdateItem(request, movie); + + Assert.Equal(new[] { "Action" }, movie.Genres); + Assert.Equal("tt1234567", movie.ProviderIds["Imdb"]); + } + + private Task InvokeUpdateItem(BaseItemDto request, BaseItem item) + { + return _subject.UpdateItem(request, item); + } +} diff --git a/tests/Jellyfin.Api.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/StartupControllerTests.cs new file mode 100644 index 0000000000..bad11a9257 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/StartupControllerTests.cs @@ -0,0 +1,70 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Controllers; +using Jellyfin.Api.Models.StartupDtos; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Server.Implementations.Users; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers; + +public class StartupControllerTests +{ + private readonly StartupController _subject; + private readonly Mock<IUserManager> _mockUserManager; + private readonly Mock<IServerConfigurationManager> _mockConfig; + + public StartupControllerTests() + { + _mockUserManager = new Mock<IUserManager>(); + _mockConfig = new Mock<IServerConfigurationManager>(); + _subject = new StartupController(_mockConfig.Object, _mockUserManager.Object); + } + + private static User CreateUser() + => new User( + "jellyfin", + typeof(DefaultAuthenticationProvider).FullName!, + typeof(DefaultPasswordResetProvider).FullName!); + + [Fact] + public async Task UpdateStartupUser_WhenNoUserExists_ReturnsNotFound() + { + _mockUserManager.Setup(m => m.GetFirstUser()).Returns((User?)null); + + var result = await _subject.UpdateStartupUser(new StartupUserDto { Name = "admin", Password = "pw" }); + + Assert.IsType<NotFoundResult>(result); + } + + [Fact] + public async Task UpdateStartupUser_WhenPasswordAlreadyConfigured_ReturnsForbidden() + { + var user = CreateUser(); + user.Password = "already-set-hash"; + _mockUserManager.Setup(m => m.GetFirstUser()).Returns(user); + + var result = await _subject.UpdateStartupUser(new StartupUserDto { Name = "attacker", Password = "new-pw" }); + + // The startup wizard must never overwrite the password of an already-provisioned + // account, even if IsStartupWizardCompleted has been cleared. + Assert.IsType<ForbidResult>(result); + _mockUserManager.Verify(m => m.ChangePassword(It.IsAny<System.Guid>(), It.IsAny<string>()), Times.Never); + } + + [Fact] + public async Task UpdateStartupUser_WhenNoPasswordYet_SetsPassword() + { + var user = CreateUser(); + Assert.True(string.IsNullOrEmpty(user.Password)); + _mockUserManager.Setup(m => m.GetFirstUser()).Returns(user); + + var result = await _subject.UpdateStartupUser(new StartupUserDto { Name = "jellyfin", Password = "first-pw" }); + + Assert.IsType<NoContentResult>(result); + _mockUserManager.Verify(m => m.ChangePassword(user.Id, "first-pw"), Times.Once); + } +} |
