diff options
| author | Dominik <git@secnd.me> | 2023-06-15 19:38:42 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-15 19:38:42 +0200 |
| commit | 17f1e8d19b1fd693893d66d2275ed8ae2476344e (patch) | |
| tree | 7f48be975faa92042769870957587b3c7864f631 /tests | |
| parent | e8ae7e5c38e28f13fa8de295e26c930cb46d9b79 (diff) | |
| parent | 6771b5cabe96b4b3cbd1cd0c998d564f3dd17ed4 (diff) | |
Merge branch 'master' into segment-deletion
Diffstat (limited to 'tests')
91 files changed, 2702 insertions, 753 deletions
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 000000000..de8fc1bb8 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,23 @@ +<Project> + <!-- Sets defaults for all test projects --> + + <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <IsPackable>false</IsPackable> + <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <!-- Code Analyzers --> + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> + <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs index 7c85ddd62..ad8a051fd 100644 --- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Claims; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Data.Entities; using Jellyfin.Server.Implementations.Security; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; @@ -51,6 +55,32 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy Assert.True(context.HasSucceeded); } + [Fact] + public async Task ShouldSucceedOnApiKey() + { + TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); + + _httpContextAccessor + .Setup(h => h.HttpContext!.Connection.RemoteIpAddress) + .Returns(new IPAddress(0)); + + _userManagerMock + .Setup(u => u.GetUserById(It.IsAny<Guid>())) + .Returns<User>(null); + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + var context = new AuthorizationHandlerContext(_requirements, principal, null); + + await _sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + [Theory] [MemberData(nameof(GetParts_ValidAuthHeader_Success_Data))] public void GetParts_ValidAuthHeader_Success(string input, Dictionary<string, string> parts) diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs index ee42216e4..1ea1797ba 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; +using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; @@ -11,25 +11,25 @@ using Microsoft.AspNetCore.Http; using Moq; using Xunit; -namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy +namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy { - public class FirstTimeSetupOrElevatedHandlerTests + public class FirstTimeSetupHandlerTests { private readonly Mock<IConfigurationManager> _configurationManagerMock; private readonly List<IAuthorizationRequirement> _requirements; - private readonly FirstTimeSetupOrElevatedHandler _sut; + private readonly FirstTimeSetupHandler _firstTimeSetupHandler; private readonly Mock<IUserManager> _userManagerMock; private readonly Mock<IHttpContextAccessor> _httpContextAccessor; - public FirstTimeSetupOrElevatedHandlerTests() + public FirstTimeSetupHandlerTests() { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); - _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() }; + _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupRequirement() }; _userManagerMock = fixture.Freeze<Mock<IUserManager>>(); _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>(); - _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>(); + _firstTimeSetupHandler = fixture.Create<FirstTimeSetupHandler>(); } [Theory] @@ -46,7 +46,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy var context = new AuthorizationHandlerContext(_requirements, claims, null); - await _sut.HandleAsync(context); + await _firstTimeSetupHandler.HandleAsync(context); Assert.True(context.HasSucceeded); } @@ -64,7 +64,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy var context = new AuthorizationHandlerContext(_requirements, claims, null); - await _sut.HandleAsync(context); + await _firstTimeSetupHandler.HandleAsync(context); Assert.Equal(shouldSucceed, context.HasSucceeded); } } diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index 7150c90bb..9cf8f8548 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { private readonly Mock<IConfigurationManager> _configurationManagerMock; private readonly List<IAuthorizationRequirement> _requirements; - private readonly IgnoreParentalControlHandler _sut; + private readonly DefaultAuthorizationHandler _sut; private readonly Mock<IUserManager> _userManagerMock; private readonly Mock<IHttpContextAccessor> _httpContextAccessor; @@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); - _requirements = new List<IAuthorizationRequirement> { new IgnoreParentalControlRequirement() }; + _requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement(validateParentalSchedule: false) }; _userManagerMock = fixture.Freeze<Mock<IUserManager>>(); _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>(); - _sut = fixture.Create<IgnoreParentalControlHandler>(); + _sut = fixture.Create<DefaultAuthorizationHandler>(); } [Theory] diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs deleted file mode 100644 index 5b3d784ff..000000000 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using AutoFixture; -using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.LocalAccessPolicy; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy -{ - public class LocalAccessHandlerTests - { - private readonly Mock<IConfigurationManager> _configurationManagerMock; - private readonly List<IAuthorizationRequirement> _requirements; - private readonly LocalAccessHandler _sut; - private readonly Mock<IUserManager> _userManagerMock; - private readonly Mock<IHttpContextAccessor> _httpContextAccessor; - private readonly Mock<INetworkManager> _networkManagerMock; - - public LocalAccessHandlerTests() - { - var fixture = new Fixture().Customize(new AutoMoqCustomization()); - _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); - _requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() }; - _userManagerMock = fixture.Freeze<Mock<IUserManager>>(); - _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>(); - _networkManagerMock = fixture.Freeze<Mock<INetworkManager>>(); - - _sut = fixture.Create<LocalAccessHandler>(); - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, false)] - public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed) - { - _networkManagerMock - .Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>())) - .Returns(isInLocalNetwork); - - TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var claims = TestHelpers.SetupUser( - _userManagerMock, - _httpContextAccessor, - UserRoles.User); - - var context = new AuthorizationHandlerContext(_requirements, claims, null); - await _sut.HandleAsync(context); - Assert.Equal(shouldSucceed, context.HasSucceeded); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs deleted file mode 100644 index ffe88fcde..000000000 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using AutoFixture; -using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.RequiresElevationPolicy; -using Jellyfin.Api.Constants; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy -{ - public class RequiresElevationHandlerTests - { - private readonly Mock<IConfigurationManager> _configurationManagerMock; - private readonly List<IAuthorizationRequirement> _requirements; - private readonly RequiresElevationHandler _sut; - private readonly Mock<IUserManager> _userManagerMock; - private readonly Mock<IHttpContextAccessor> _httpContextAccessor; - - public RequiresElevationHandlerTests() - { - var fixture = new Fixture().Customize(new AutoMoqCustomization()); - _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); - _requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() }; - _userManagerMock = fixture.Freeze<Mock<IUserManager>>(); - _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>(); - - _sut = fixture.Create<RequiresElevationHandler>(); - } - - [Theory] - [InlineData(UserRoles.Administrator, true)] - [InlineData(UserRoles.User, false)] - [InlineData(UserRoles.Guest, false)] - public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) - { - TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); - var claims = TestHelpers.SetupUser( - _userManagerMock, - _httpContextAccessor, - role); - - var context = new AuthorizationHandlerContext(_requirements, claims, null); - - await _sut.HandleAsync(context); - Assert.Equal(shouldSucceed, context.HasSucceeded); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs new file mode 100644 index 000000000..0254a1ec6 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs @@ -0,0 +1,35 @@ +using Jellyfin.Api.Controllers; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers; + +public static class ImageControllerTests +{ + [Theory] + [InlineData("image/apng", ".apng")] + [InlineData("image/avif", ".avif")] + [InlineData("image/bmp", ".bmp")] + [InlineData("image/gif", ".gif")] + [InlineData("image/x-icon", ".ico")] + [InlineData("image/jpeg", ".jpg")] + [InlineData("image/png", ".png")] + [InlineData("image/png; charset=utf-8", ".png")] + [InlineData("image/svg+xml", ".svg")] + [InlineData("image/tiff", ".tiff")] + [InlineData("image/webp", ".webp")] + public static void TryGetImageExtensionFromContentType_Valid_True(string contentType, string extension) + { + Assert.True(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex)); + Assert.Equal(extension, ex); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("text/html")] + public static void TryGetImageExtensionFromContentType_InValid_False(string contentType) + { + Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex)); + Assert.Null(ex); + } +} diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index c4640bd22..2d7741d81 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Security.Claims; +using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Net; using Xunit; namespace Jellyfin.Api.Tests.Helpers @@ -15,6 +19,82 @@ namespace Jellyfin.Api.Tests.Helpers Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder)); } + [Fact] + public static void GetUserId_IsAdmin() + { + Guid? requestUserId = Guid.NewGuid(); + Guid? authUserId = Guid.NewGuid(); + + var claims = new[] + { + new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.IsApiKey, bool.FalseString), + new Claim(ClaimTypes.Role, UserRoles.Administrator) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(requestUserId, userId); + } + + [Fact] + public static void GetUserId_IsApiKey_EmptyGuid() + { + Guid? requestUserId = Guid.Empty; + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(Guid.Empty, userId); + } + + [Fact] + public static void GetUserId_IsApiKey_Null() + { + Guid? requestUserId = null; + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(Guid.Empty, userId); + } + + [Fact] + public static void GetUserId_IsUser() + { + Guid? requestUserId = Guid.NewGuid(); + Guid? authUserId = Guid.NewGuid(); + + var claims = new[] + { + new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.IsApiKey, bool.FalseString), + new Claim(ClaimTypes.Role, UserRoles.User) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + Assert.Throws<SecurityException>(() => RequestHelpers.GetUserId(principal, requestUserId)); + } + public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData() { var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>(); diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index a20c9690f..015018910 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -5,37 +5,20 @@ <ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.17.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> + <PackageReference Include="Microsoft.Extensions.Options" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="Moq" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index f9bca4146..5eab3ae6f 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -36,7 +36,7 @@ namespace Jellyfin.Api.Tests user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); // Add access schedules if set. - if (accessSchedules != null) + if (accessSchedules is not null) { foreach (var accessSchedule in accessSchedules) { diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 95fc1d917..8fef7fde0 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -5,32 +5,15 @@ <ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="FsCheck.Xunit" Version="2.16.5" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="FsCheck.Xunit" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs index 463e17ad3..f67e6d1ef 100644 --- a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs +++ b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs @@ -20,17 +20,13 @@ namespace Jellyfin.Controller.Tests { BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; - var libraryOptions = new LibraryOptions - { - TypeOptions = new[] + var libraryTypeOptions = itemType == typeof(Book) + ? new TypeOptions { - new TypeOptions - { - Type = "Book", - MetadataFetchers = new[] { "LibraryEnabled" } - } + Type = "Book", + MetadataFetchers = new[] { "LibraryEnabled" } } - }; + : null; var serverConfiguration = new ServerConfiguration(); foreach (var typeConfig in serverConfiguration.MetadataOptions) @@ -43,7 +39,7 @@ namespace Jellyfin.Controller.Tests .Returns(serverConfiguration); var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); - var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryOptions, fetcherName); + var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, fetcherName); Assert.Equal(expected, actual); } @@ -57,17 +53,13 @@ namespace Jellyfin.Controller.Tests { BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!; - var libraryOptions = new LibraryOptions - { - TypeOptions = new[] + var libraryTypeOptions = itemType == typeof(Book) + ? new TypeOptions { - new TypeOptions - { - Type = "Book", - ImageFetchers = new[] { "LibraryEnabled" } - } + Type = "Book", + ImageFetchers = new[] { "LibraryEnabled" } } - }; + : null; var serverConfiguration = new ServerConfiguration(); foreach (var typeConfig in serverConfiguration.MetadataOptions) @@ -80,7 +72,7 @@ namespace Jellyfin.Controller.Tests .Returns(serverConfiguration); var baseItemManager = new BaseItemManager(serverConfigurationManager.Object); - var actual = baseItemManager.IsImageFetcherEnabled(item, libraryOptions, fetcherName); + var actual = baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, fetcherName); Assert.Equal(expected, actual); } diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs index 985bbcde1..f3ada59db 100644 --- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs +++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs @@ -1,18 +1,17 @@ using MediaBrowser.Controller.Entities; using Xunit; -namespace Jellyfin.Controller.Tests.Entities +namespace Jellyfin.Controller.Tests.Entities; + +public class BaseItemTests { - public class BaseItemTests - { - [Theory] - [InlineData("", "")] - [InlineData("1", "0000000001")] - [InlineData("t", "t")] - [InlineData("test", "test")] - [InlineData("test1", "test0000000001")] - [InlineData("1test 2", "0000000001test 0000000002")] - public void BaseItem_ModifySortChunks_Valid(string input, string expected) - => Assert.Equal(expected, BaseItem.ModifySortChunks(input)); - } + [Theory] + [InlineData("", "")] + [InlineData("1", "0000000001")] + [InlineData("t", "t")] + [InlineData("test", "test")] + [InlineData("test1", "test0000000001")] + [InlineData("1test 2", "0000000001test 0000000002")] + public void BaseItem_ModifySortChunks_Valid(string input, string expected) + => Assert.Equal(expected, BaseItem.ModifySortChunks(input)); } diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index d95747206..54d93b48c 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -5,32 +5,15 @@ <ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index 1444d6faf..69677ce42 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -1,31 +1,14 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs new file mode 100644 index 000000000..c9018fe2f --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs @@ -0,0 +1,47 @@ +using Emby.Dlna.Server; +using MediaBrowser.Model.Dlna; +using Xunit; + +namespace Jellyfin.Dlna.Server.Tests; + +public class DescriptionXmlBuilderTests +{ + [Fact] + public void GetFriendlyName_EmptyProfile_ReturnsServerName() + { + const string ServerName = "Test Server Name"; + var builder = new DescriptionXmlBuilder(new DeviceProfile(), "serverUdn", "localhost", ServerName, string.Empty); + Assert.Equal(ServerName, builder.GetFriendlyName()); + } + + [Fact] + public void GetFriendlyName_FriendlyName_ReturnsFriendlyName() + { + const string FriendlyName = "Friendly Neighborhood Test Server"; + var builder = new DescriptionXmlBuilder( + new DeviceProfile() + { + FriendlyName = FriendlyName + }, + "serverUdn", + "localhost", + "Test Server Name", + string.Empty); + Assert.Equal(FriendlyName, builder.GetFriendlyName()); + } + + [Fact] + public void GetFriendlyName_FriendlyNameInterpolation_ReturnsFriendlyName() + { + var builder = new DescriptionXmlBuilder( + new DeviceProfile() + { + FriendlyName = "Friendly Neighborhood ${HostName}" + }, + "serverUdn", + "localhost", + "Test Server Name", + string.Empty); + Assert.Equal("Friendly Neighborhood TestServerName", builder.GetFriendlyName()); + } +} diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs index 7730841a1..105e2a52a 100644 --- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs +++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs @@ -19,11 +19,16 @@ namespace Jellyfin.Extensions.Tests [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")] [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")] [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")] + [InlineData("a5", "a11")] + [InlineData("a05a", "a5b")] + [InlineData("a5a", "a05b")] + [InlineData("6xxx", "007asdf")] + [InlineData("00042Q", "42s")] public void AlphanumericComparatorTest(params string?[] strings) { var copy = strings.Reverse().ToArray(); Array.Sort(copy, new AlphanumericComparator()); - Assert.True(strings.SequenceEqual(copy)); + Assert.Equal(strings, copy); } } } diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index 9e3bef881..036489829 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -1,34 +1,17 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2"> + <PackageReference Include="coverlet.collector"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="FsCheck.Xunit" Version="2.16.5" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="FsCheck.Xunit" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs new file mode 100644 index 000000000..be256da2e --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonBoolStringTests + { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonBoolStringConverter() + } + }; + + [Theory] + [InlineData(@"{ ""Value"": ""true"" }", true)] + [InlineData(@"{ ""Value"": ""false"" }", false)] + public void Deserialize_String_Valid_Success(string input, bool output) + { + var s = JsonSerializer.Deserialize<TestStruct>(input, _jsonOptions); + Assert.Equal(s.Value, output); + } + + [Theory] + [InlineData(true, "true")] + [InlineData(false, "false")] + public void Serialize_Bool_Success(bool input, string output) + { + var value = JsonSerializer.Serialize(input, _jsonOptions); + Assert.Equal(value, output); + } + + private readonly record struct TestStruct(bool Value); + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs index af9227de2..16c69ca48 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters Assert.Equal(json, res); } - private class TestContainer + private sealed class TestContainer { public TestContainer(CollectionTypeOptions? collectionType) { diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs index 903d88caa..69d20bd3f 100644 --- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs +++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs @@ -9,12 +9,15 @@ namespace Jellyfin.Extensions.Tests [InlineData("", "")] // Identity edge-case (no diactritics) [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics) [InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping + [InlineData("åäö", "aao")] // Issue #7484 [InlineData("Jön", "Jon")] // Issue #7484 [InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484 [InlineData("Kieślowski", "Kieslowski")] // Issue #7450 [InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560 [InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support) [InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393 + [InlineData("Le cœur a ses raisons", "Le coeur a ses raisons")] // Issue #8893 + [InlineData("Béla Tarr", "Bela Tarr")] // Issue #8893 public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult) { string result = input.RemoveDiacritics(); @@ -25,12 +28,15 @@ namespace Jellyfin.Extensions.Tests [InlineData("", false)] // Identity edge-case (no diactritics) [InlineData("Indiana Jones", false)] // Identity (no diactritics) [InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping + [InlineData("åäö", true)] // Issue #7484 [InlineData("Jön", true)] // Issue #7484 [InlineData("Jönssonligan", true)] // Issue #7484 [InlineData("Kieślowski", true)] // Issue #7450 [InlineData("Cidadão Kane", true)] // Issue #7560 [InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support) [InlineData("애타는 로맨스", false)] // Issue #6393 + [InlineData("Le cœur a ses raisons", true)] // Issue #8893 + [InlineData("Béla Tarr", true)] // Issue #8893 public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult) { bool result = input.HasDiacritics(); diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index 83ea1907c..eab003715 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -1,34 +1,18 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2"> + <PackageReference Include="coverlet.collector"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> - <!-- Code Analyzers --> - <ItemGroup> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" /> <ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Keyframes\Jellyfin.MediaEncoding.Keyframes.csproj" /> diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index 84a069424..894bec6aa 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -1,36 +1,18 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - <RootNamespace>Jellyfin.MediaEncoding.Keyframes</RootNamespace> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2"> + <PackageReference Include="coverlet.collector"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="../../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" /> </ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs index c0c363d3d..db7e91c6a 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -17,6 +17,8 @@ namespace Jellyfin.MediaEncoding.Tests } [Theory] + [InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)] @@ -32,10 +34,12 @@ namespace Jellyfin.MediaEncoding.Tests Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput)); } - private class GetFFmpegVersionTestData : TheoryData<string, Version?> + private sealed class GetFFmpegVersionTestData : TheoryData<string, Version?> { public GetFFmpegVersionTestData() { + Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0)); + Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2)); Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4)); Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2)); Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1)); diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs index 02bf046ed..89ba42da0 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs @@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests { internal static class EncoderValidatorTestsData { + public const string FFmpegV60Output = @"ffmpeg version 6.0-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers +built with gcc 12.2.0 (crosstool-NG 1.25.0.90_cf9beb1) +configuration: --prefix=/ffbuild/prefix --pkg-config=pkg-config --pkg-config-flags=--static --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --extra-version=Jellyfin --extra-cflags= --extra-cxxflags= --extra-ldflags= --extra-ldexeflags= --extra-libs= --enable-gpl --enable-version3 --enable-lto --disable-ffplay --disable-debug --disable-doc --disable-ptx-compression --disable-sdl2 --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-amf --enable-chromaprint --enable-libdav1d --enable-dxva2 --enable-d3d11va --enable-libfdk-aac --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-libvpl --enable-schannel --enable-libsrt --enable-libsvtav1 --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libzimg --enable-libzvbi +libavutil 58. 2.100 / 58. 2.100 +libavcodec 60. 3.100 / 60. 3.100 +libavformat 60. 3.100 / 60. 3.100 +libavdevice 60. 1.100 / 60. 1.100 +libavfilter 9. 3.100 / 9. 3.100 +libswscale 7. 1.100 / 7. 1.100 +libswresample 4. 10.100 / 4. 10.100 +libpostproc 57. 1.100 / 57. 1.100"; + + public const string FFmpegV512Output = @"ffmpeg version 5.1.2-Jellyfin Copyright (c) 2000-2022 the FFmpeg developers +built with gcc 10-win32 (GCC) 20220324 +configuration: --prefix=/opt/ffmpeg --arch=x86_64 --target-os=mingw32 --cross-prefix=x86_64-w64-mingw32- --pkg-config=pkg-config --pkg-config-flags=--static --extra-libs='-lfftw3f -lstdc++' --extra-cflags=-DCHROMAPRINT_NODLL --extra-version=Jellyfin --disable-ffplay --disable-debug --disable-doc --disable-sdl2 --disable-ptx-compression --disable-w32threads --enable-pthreads --enable-shared --enable-lto --enable-gpl --enable-version3 --enable-schannel --enable-iconv --enable-libxml2 --enable-zlib --enable-lzma --enable-gmp --enable-chromaprint --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libzimg --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libdav1d --enable-libfdk-aac --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc +libavutil 57. 28.100 / 57. 28.100 +libavcodec 59. 37.100 / 59. 37.100 +libavformat 59. 27.100 / 59. 27.100 +libavdevice 59. 7.100 / 59. 7.100 +libavfilter 8. 44.100 / 8. 44.100 +libswscale 6. 7.100 / 6. 7.100 +libswresample 4. 7.100 / 4. 7.100 +libpostproc 56. 6.100 / 56. 6.100"; + public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers built with gcc 10.3.0 (Rev5, Built by MSYS2 project) configuration: --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs deleted file mode 100644 index 97dbb3be0..000000000 --- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.IO; -using System.Text.Json; -using System.Threading.Tasks; -using Jellyfin.Extensions.Json; -using MediaBrowser.MediaEncoding.Probing; -using MediaBrowser.Model.IO; -using Xunit; - -namespace Jellyfin.MediaEncoding.Tests -{ - public class FFprobeParserTests - { - [Theory] - [InlineData("ffprobe1.json")] - public async Task Test(string fileName) - { - var path = Path.Join("Test Data", fileName); - await using (var stream = AsyncFile.OpenRead(path)) - { - var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false); - Assert.NotNull(res); - } - } - } -} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 4cff2143c..6b703e741 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -5,12 +5,6 @@ <ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <None Include="Test Data\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -18,30 +12,19 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.17.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" /> </ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index bbe1246ca..198dc63ef 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -2,7 +2,9 @@ using System; using System.Globalization; using System.IO; using System.Text.Json; +using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -15,9 +17,15 @@ namespace Jellyfin.MediaEncoding.Tests.Probing { public class ProbeResultNormalizerTests { - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly JsonSerializerOptions _jsonOptions; private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null); + public ProbeResultNormalizerTests() + { + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); + _jsonOptions.Converters.Add(new JsonBoolStringConverter()); + } + [Theory] [InlineData("2997/125", 23.976f)] [InlineData("1/50", 0.02f)] @@ -31,16 +39,6 @@ namespace Jellyfin.MediaEncoding.Tests.Probing public void GetFrameRate_Success(string value, float? expected) => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); - [Theory] - [InlineData(0.5f, "0/1", false)] - [InlineData(24.5f, "8/196", false)] - [InlineData(63.5f, "1/127", true)] - [InlineData(null, "1/60", false)] - [InlineData(30f, "2/120", true)] - [InlineData(59.999996f, "1563/187560", true)] - public void IsCodecTimeBaseDoubleTheFrameRate_Success(float? frameRate, string codecTimeBase, bool expected) - => Assert.Equal(expected, ProbeResultNormalizer.IsCodecTimeBaseDoubleTheFrameRate(frameRate, codecTimeBase)); - [Fact] public void GetMediaInfo_MetaData_Success() { @@ -159,6 +157,112 @@ namespace Jellyfin.MediaEncoding.Tests.Probing } [Fact] + public void GetMediaInfo_TS_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_ts.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File); + + Assert.Equal(2, res.MediaStreams.Count); + + Assert.False(res.MediaStreams[0].IsAVC); + } + + [Fact] + public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_progressive_no_field_order.json"); + + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_progressive_no_field_order.mp4", MediaProtocol.File); + + Assert.Equal(2, res.MediaStreams.Count); + + Assert.NotNull(res.VideoStream); + Assert.Equal(res.MediaStreams[0], res.VideoStream); + Assert.Equal(0, res.VideoStream.Index); + Assert.Equal("h264", res.VideoStream.Codec); + Assert.Equal("Main", res.VideoStream.Profile); + Assert.Equal(MediaStreamType.Video, res.VideoStream.Type); + Assert.Equal(1080, res.VideoStream.Height); + Assert.Equal(1920, res.VideoStream.Width); + Assert.False(res.VideoStream.IsInterlaced); + Assert.Equal("16:9", res.VideoStream.AspectRatio); + Assert.Equal("yuv420p", res.VideoStream.PixelFormat); + Assert.Equal(41d, res.VideoStream.Level); + Assert.Equal(1, res.VideoStream.RefFrames); + Assert.True(res.VideoStream.IsAVC); + Assert.Equal(23.9760246f, res.VideoStream.RealFrameRate); + Assert.Equal("1/24000", res.VideoStream.TimeBase); + Assert.Equal(3948341, res.VideoStream.BitRate); + Assert.Equal(8, res.VideoStream.BitDepth); + Assert.True(res.VideoStream.IsDefault); + } + + [Fact] + public void GetMediaInfo_ProgressiveVideoNoFieldOrder2_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_progressive_no_field_order2.json"); + + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_progressive_no_field_order2.mp4", MediaProtocol.File); + + Assert.Single(res.MediaStreams); + + Assert.NotNull(res.VideoStream); + Assert.Equal(res.MediaStreams[0], res.VideoStream); + Assert.Equal(0, res.VideoStream.Index); + Assert.Equal("h264", res.VideoStream.Codec); + Assert.Equal("High", res.VideoStream.Profile); + Assert.Equal(MediaStreamType.Video, res.VideoStream.Type); + Assert.Equal(720, res.VideoStream.Height); + Assert.Equal(1280, res.VideoStream.Width); + Assert.False(res.VideoStream.IsInterlaced); + Assert.Equal("16:9", res.VideoStream.AspectRatio); + Assert.Equal("yuv420p", res.VideoStream.PixelFormat); + Assert.Equal(31d, res.VideoStream.Level); + Assert.Equal(1, res.VideoStream.RefFrames); + Assert.True(res.VideoStream.IsAVC); + Assert.Equal(25f, res.VideoStream.RealFrameRate); + Assert.Equal("1/12800", res.VideoStream.TimeBase); + Assert.Equal(53288, res.VideoStream.BitRate); + Assert.Equal(8, res.VideoStream.BitDepth); + Assert.True(res.VideoStream.IsDefault); + } + + [Fact] + public void GetMediaInfo_InterlacedVideo_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_interlaced.json"); + + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_interlaced.mp4", MediaProtocol.File); + + Assert.Single(res.MediaStreams); + + Assert.NotNull(res.VideoStream); + Assert.Equal(res.MediaStreams[0], res.VideoStream); + Assert.Equal(0, res.VideoStream.Index); + Assert.Equal("h264", res.VideoStream.Codec); + Assert.Equal("High", res.VideoStream.Profile); + Assert.Equal(MediaStreamType.Video, res.VideoStream.Type); + Assert.Equal(720, res.VideoStream.Height); + Assert.Equal(1280, res.VideoStream.Width); + Assert.True(res.VideoStream.IsInterlaced); + Assert.Equal("16:9", res.VideoStream.AspectRatio); + Assert.Equal("yuv420p", res.VideoStream.PixelFormat); + Assert.Equal(40d, res.VideoStream.Level); + Assert.Equal(1, res.VideoStream.RefFrames); + Assert.True(res.VideoStream.IsAVC); + Assert.Equal(25f, res.VideoStream.RealFrameRate); + Assert.Equal("1/12800", res.VideoStream.TimeBase); + Assert.Equal(56945, res.VideoStream.BitRate); + Assert.Equal(8, res.VideoStream.BitDepth); + Assert.True(res.VideoStream.IsDefault); + } + + [Fact] public void GetMediaInfo_MusicVideo_Success() { var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json"); @@ -211,15 +315,15 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate); Assert.Equal(22, res.People.Length); Assert.Equal("Krysta Youngs", res.People[0].Name); - Assert.Equal(PersonType.Composer, res.People[0].Type); + Assert.Equal(PersonKind.Composer, res.People[0].Type); Assert.Equal("Julia Ross", res.People[1].Name); - Assert.Equal(PersonType.Composer, res.People[1].Type); + Assert.Equal(PersonKind.Composer, res.People[1].Type); Assert.Equal("Yiwoomin", res.People[2].Name); - Assert.Equal(PersonType.Composer, res.People[2].Type); + Assert.Equal(PersonKind.Composer, res.People[2].Type); Assert.Equal("Ji-hyo Park", res.People[3].Name); - Assert.Equal(PersonType.Lyricist, res.People[3].Type); + Assert.Equal(PersonKind.Lyricist, res.People[3].Type); Assert.Equal("Yiwoomin", res.People[4].Name); - Assert.Equal(PersonType.Actor, res.People[4].Type); + Assert.Equal(PersonKind.Actor, res.People[4].Type); Assert.Equal("Electric Piano", res.People[4].Role); Assert.Equal(4, res.Genres.Length); Assert.Contains("Electronic", res.Genres); diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs index 243127438..9ace80bbd 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs @@ -26,7 +26,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.ass", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.ass", + Protocol = MediaProtocol.File, + Format = "ass", + IsExternal = true + }); data.Add( new MediaSourceInfo() @@ -38,7 +44,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.ssa", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.ssa", + Protocol = MediaProtocol.File, + Format = "ssa", + IsExternal = true + }); data.Add( new MediaSourceInfo() @@ -50,7 +62,13 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.srt", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.srt", + Protocol = MediaProtocol.File, + Format = "srt", + IsExternal = true + }); data.Add( new MediaSourceInfo() @@ -62,14 +80,20 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests Path = "/media/sub.ass", IsExternal = true }, - new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true)); + new SubtitleEncoder.SubtitleInfo() + { + Path = "/media/sub.ass", + Protocol = MediaProtocol.File, + Format = "ass", + IsExternal = true + }); return data; } [Theory] [MemberData(nameof(GetReadableFile_Valid_TestData))] - internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo) + public async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo) { var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); var subtitleEncoder = fixture.Create<SubtitleEncoder>(); diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json new file mode 100644 index 000000000..810244920 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json @@ -0,0 +1,81 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1280, + "height": 720, + "coded_width": 1280, + "coded_height": 720, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 2, + "pix_fmt": "yuv420p", + "level": 40, + "chroma_location": "left", + "field_order": "tt", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "id": "0x1", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/12800", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 3840000, + "duration": "300.000000", + "bit_rate": "56945", + "bits_per_raw_sample": "8", + "nb_frames": "7500", + "extradata_size": 42, + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "und", + "handler_name": "VideoHandler", + "vendor_id": "[0][0][0][0]" + } + } + ], + "format": { + "filename": "test-gray.720i.mp4", + "nb_streams": 1, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "300.000000", + "size": "2223957", + "bit_rate": "59305", + "probe_score": 100, + "tags": { + "major_brand": "isom", + "minor_version": "512", + "compatible_brands": "isomiso2avc1mp41", + "encoder": "Lavf58.20.100" + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json new file mode 100644 index 000000000..897c5e3ab --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json @@ -0,0 +1,133 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1001/48000", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1088, + "closed_captions": 0, + "has_b_frames": 1, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p", + "level": 41, + "chroma_location": "left", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "24000/1001", + "avg_frame_rate": "24000/1001", + "time_base": "1/24000", + "start_pts": 1000, + "start_time": "0.041667", + "duration_ts": 29095066, + "duration": "1212.294417", + "bit_rate": "3948341", + "bits_per_raw_sample": "8", + "nb_frames": "29066", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2020-01-20T13:56:34.000000Z", + "language": "eng", + "handler_name": "\fVideoHandler", + "encoder": "h264" + } + }, + { + "index": 1, + "codec_name": "ac3", + "codec_long_name": "ATSC A/52A (AC-3)", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "ac-3", + "codec_tag": "0x332d6361", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "dmix_mode": "-1", + "ltrt_cmixlev": "-1.000000", + "ltrt_surmixlev": "-1.000000", + "loro_cmixlev": "-1.000000", + "loro_surmixlev": "-1.000000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 58232832, + "duration": "1213.184000", + "bit_rate": "224000", + "nb_frames": "37912", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "creation_time": "2020-01-20T13:56:34.000000Z", + "language": "eng", + "handler_name": "\fSoundHandler" + }, + "side_data_list": [ + { + "side_data_type": "Audio Service Type" + } + ] + } + ], + "format": { + "filename": "The Big Bang Theory - S01E17.mp4", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "1213.184000", + "size": "633084606", + "bit_rate": "4174698", + "probe_score": 100, + "tags": { + "major_brand": "mp42", + "minor_version": "512", + "compatible_brands": "mp42", + "creation_time": "2020-01-20T13:56:34.000000Z", + "media_type": "9", + "season_number": "0", + "episode_sort": "0", + "hd_video": "0", + "iTunMOVI": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>studio</key><string>studio</string><key>cast</key><array><dict><key>name</key><string></string></dict></array><key>directors</key><array><dict><key>name</key><string></string></dict></array><key>producers</key><array><dict><key>name</key><string></string></dict></array><key>codirectors</key><array><dict><key>name</key><string>codirector</string></dict></array><key>screenwriters</key><array><dict><key>name</key><string></string></dict></array></dict></plist>" + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json new file mode 100644 index 000000000..4a03e0d61 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json @@ -0,0 +1,72 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_time_base": "1/50", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1280, + "height": 720, + "coded_width": 1280, + "coded_height": 720, + "closed_captions": 0, + "has_b_frames": 2, + "pix_fmt": "yuv420p", + "level": 31, + "chroma_location": "left", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/12800", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 3840000, + "duration": "300.000000", + "bit_rate": "53288", + "bits_per_raw_sample": "8", + "nb_frames": "7500", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "und", + "handler_name": "VideoHandler" + } + } + ], + "format": { + "filename": "test-gray.720p.mp4", + "nb_streams": 1, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "300.000000", + "size": "2086818", + "bit_rate": "55648", + "probe_score": 100, + "tags": { + "major_brand": "isom", + "minor_version": "512", + "compatible_brands": "isomiso2avc1mp41", + "encoder": "Lavf58.20.100" + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json index cdad5df50..cdad5df50 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index c279b6b4b..c30dad6f9 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -37,7 +37,7 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -88,7 +88,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -162,9 +162,9 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] - public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); } @@ -176,7 +176,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -186,7 +186,7 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -260,9 +260,9 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] - public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); options.AudioStreamIndex = 1; options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; @@ -275,10 +275,10 @@ namespace Jellyfin.Model.Tests // Chrome [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Firefox [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 @@ -296,9 +296,9 @@ namespace Jellyfin.Model.Tests // Tizen 4 4K 5.1 [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] - public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") + public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); var streamCount = options.MediaSources[0].MediaStreams.Count; if (streamCount > 0) { @@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); } - private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) { if (string.IsNullOrEmpty(transcodeProtocol)) { @@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests var builder = GetStreamBuilder(); - var val = builder.BuildVideoItem(options); - Assert.NotNull(val); + var streamInfo = builder.GetOptimalVideoStream(options); + Assert.NotNull(streamInfo); - if (playMethod != null) + if (playMethod is not null) { - Assert.Equal(playMethod, val.PlayMethod); + Assert.Equal(playMethod, streamInfo.PlayMethod); } - Assert.Equal(why, val.TranscodeReasons); + Assert.Equal(why, streamInfo.TranscodeReasons); var audioStreamIndexInput = options.AudioStreamIndex; - var targetVideoStream = val.TargetVideoStream; - var targetAudioStream = val.TargetAudioStream; + var targetVideoStream = streamInfo.TargetVideoStream; + var targetAudioStream = streamInfo.TargetAudioStream; - var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); + var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId); Assert.NotNull(mediaSource); var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); // TODO: Check AudioStreamIndex vs options.AudioStreamIndex var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); - var uri = ParseUri(val); + var uri = ParseUri(streamInfo); if (playMethod == PlayMethod.DirectPlay) { @@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); - Assert.Single(val.TargetAudioCodec); + Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec); + Assert.Single(streamInfo.TargetAudioCodec); // Assert.Single(val.AudioCodecs); - if (transcodeMode == "DirectStream") + if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); } } else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) { - Assert.NotNull(val.Container); - Assert.NotEmpty(val.VideoCodecs); - Assert.NotEmpty(val.AudioCodecs); + Assert.NotNull(streamInfo.Container); + Assert.NotEmpty(streamInfo.VideoCodecs); + Assert.NotEmpty(streamInfo.AudioCodecs); // Check expected container (todo: this could be a test param) - if (transcodeProtocol == "http") + if (transcodeProtocol.Equals("http", StringComparison.Ordinal)) { // Assert.Equal("webm", val.Container); - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); Assert.Equal("stream", uri.Filename); - Assert.Equal("http", val.SubProtocol); + Assert.Equal("http", streamInfo.SubProtocol); } - else if (transcodeProtocol == "HLS.mp4") + else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal)) { - Assert.Equal("mp4", val.Container); + Assert.Equal("mp4", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } else { - Assert.Equal("ts", val.Container); + Assert.Equal("ts", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } // Full transcode - if (transcodeMode == "Transcode") + if (transcodeMode.Equals("Transcode", StringComparison.Ordinal)) { - if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) { Assert.All( videoStreams, - stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs)); } - // TODO: Fill out tests here + // TODO: fill out tests here } // DirectStream and Remux else { // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); - if (transcodeMode == "DirectStream") + if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { // Check expected audio codecs (1) if (!targetAudioStream.IsExternal) { - if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) + // Check expected audio codecs (1) + if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) { - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); } else { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } } - else if (transcodeMode == "Remux") + else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) { // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); - Assert.Single(val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); + Assert.Single(streamInfo.AudioCodecs); } // Video details var videoStream = targetVideoStream; - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); - Assert.Equal(videoStream.Level, val.TargetVideoLevel); - Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); - Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); + Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); + Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel); + Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth); + Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); // Audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != 0) @@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests // TODO:fixme if (!targetAudioStream.IsExternal) { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } @@ -465,23 +466,23 @@ namespace Jellyfin.Model.Tests { if (!stream.IsExternal) { - Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs); } }); } } } } - else if (playMethod == null) + else if (playMethod is null) { - Assert.Null(val.SubProtocol); + Assert.Null(streamInfo.SubProtocol); Assert.Equal("stream", uri.Filename); - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); } - return val; + return streamInfo; } private static async ValueTask<T> TestData<T>(string name) @@ -490,7 +491,7 @@ namespace Jellyfin.Model.Tests using (var stream = File.OpenRead(path)) { var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options); - if (value != null) + if (value is not null) { return value; } @@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests return new StreamBuilder(transcodeSupport.Object, logger); } - private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources) + private static async ValueTask<MediaOptions> GetMediaOptions(string deviceProfile, params string[] sources) { var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src)) .Select(val => val.Result) @@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests var dp = await TestData<DeviceProfile>(deviceProfile); - return new VideoOptions() + return new MediaOptions() { ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), MediaSourceId = mediaSourceId, diff --git a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs index a1ace8476..2a62ab74c 100644 --- a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs @@ -186,7 +186,7 @@ namespace Jellyfin.Model.Tests.Entities Assert.Null(nullProvider.ProviderIds); } - private class ProviderIdsExtensionsTestsObject : IHasProviderIds + private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds { public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject(); diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index fbcaa66f4..8345b610e 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -1,21 +1,15 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="FsCheck.Xunit" Version="2.16.5" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="FsCheck.Xunit" /> </ItemGroup> <ItemGroup> @@ -24,17 +18,6 @@ </None> </ItemGroup> - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" /> </ItemGroup> diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs index cbab455f0..371c3811a 100644 --- a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs +++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs @@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net [InlineData("image/jpeg", ".jpg")] [InlineData("image/png", ".png")] [InlineData("image/svg+xml", ".svg")] - [InlineData("image/tiff", ".tif")] + [InlineData("image/tiff", ".tiff")] [InlineData("image/vnd.microsoft.icon", ".ico")] [InlineData("image/webp", ".webp")] + [InlineData("image/x-icon", ".ico")] [InlineData("image/x-png", ".png")] [InlineData("text/css", ".css")] [InlineData("text/csv", ".csv")] diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs index 58aaed023..c49663248 100644 --- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs +++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs @@ -12,8 +12,6 @@ namespace Jellyfin.Naming.Tests.Common Assert.NotEmpty(options.CleanDateTimeRegexes); Assert.NotEmpty(options.CleanStringRegexes); - Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes); - Assert.NotEmpty(options.EpisodeMultiPartRegexes); } [Fact] diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index da0e9f5b1..112dd780e 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -5,36 +5,19 @@ <ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> + <PackageReference Include="coverlet.collector" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" /> </ItemGroup> - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - </Project> diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs index 68059f980..406381f14 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs @@ -73,6 +73,11 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number [InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)] + [InlineData("Season 3/The Series Season 3 Episode 9 - The title.avi", 9)] + [InlineData("Season 3/The Series S3 E9 - The title.avi", 9)] + [InlineData("Season 3/S003 E009.avi", 9)] + [InlineData("Season 3/Season 3 Episode 9.avi", 9)] + // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)] // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)] diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index af219b118..7604ddc80 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)] [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)] [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)] + [InlineData("/The.Sopranos/Season 3/The Sopranos Season 3 Episode 09 - The Telltale Moozadell.avi", false, "The Sopranos", 3, 9)] // TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)] // TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)] // TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)] diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs index b7b5b54ec..55af33836 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Naming.Tests.TV { var result = SeasonPathParser.Parse(path, true, true); - Assert.Equal(result.SeasonNumber != null, result.Success); + Assert.Equal(result.SeasonNumber is not null, result.Success); Assert.Equal(result.SeasonNumber, seasonNumber); Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); } diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index 1574bce58..6c9c98cbe 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -10,6 +10,7 @@ namespace Jellyfin.Naming.Tests.Video [Theory] [InlineData("Super movie 480p.mp4", "Super movie")] + [InlineData("Super movie Multi.mp4", "Super movie")] [InlineData("Super movie 480p 2001.mp4", "Super movie")] [InlineData("Super movie [480p].mp4", "Super movie")] [InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")] diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs index 1762b91b9..511a014a6 100644 --- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(is3D, result.Is3D); - if (format3D == null) + if (format3D is null) { Assert.Null(result?.Format3D); } diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 79f2366b8..294f11ee7 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), _namingOptions).ToList(); - Assert.Single(result.Where(v => v.ExtraType == null)); - Assert.Single(result.Where(v => v.ExtraType != null)); + Assert.Single(result.Where(v => v.ExtraType is null)); + Assert.Single(result.Where(v => v.ExtraType is not null)); } [Fact] @@ -44,8 +44,8 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), _namingOptions).ToList(); - Assert.Single(result.Where(v => v.ExtraType == null)); - Assert.Single(result.Where(v => v.ExtraType != null)); + Assert.Single(result.Where(v => v.ExtraType is null)); + Assert.Single(result.Where(v => v.ExtraType is not null)); Assert.Equal(2, result[0].AlternateVersions.Count); } @@ -188,8 +188,7 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man-bluray.mkv", @"/movies/Iron Man/Iron Man-3d.mkv", @"/movies/Iron Man/Iron Man-3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man-3d.hsbs.mkv", - @"/movies/Iron Man/Iron Man[test].mkv", + @"/movies/Iron Man/Iron Man[test].mkv" }; var result = VideoListResolver.Resolve( @@ -197,10 +196,14 @@ namespace Jellyfin.Naming.Tests.Video _namingOptions).ToList(); Assert.Single(result); - Assert.Equal(7, result[0].AlternateVersions.Count); - Assert.False(result[0].AlternateVersions[2].Is3D); - Assert.True(result[0].AlternateVersions[3].Is3D); - Assert.True(result[0].AlternateVersions[4].Is3D); + Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path); + Assert.Equal(6, result[0].AlternateVersions.Count); + Assert.Equal("/movies/Iron Man/Iron Man-720p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/Iron Man/Iron Man-3d.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/Iron Man/Iron Man-3d-hsbs.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/Iron Man/Iron Man-bluray.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/Iron Man/Iron Man-test.mkv", result[0].AlternateVersions[4].Path); + Assert.Equal("/movies/Iron Man/Iron Man[test].mkv", result[0].AlternateVersions[5].Path); } [Fact] @@ -214,7 +217,6 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man - bluray.mkv", @"/movies/Iron Man/Iron Man - 3d.mkv", @"/movies/Iron Man/Iron Man - 3d-hsbs.mkv", - @"/movies/Iron Man/Iron Man - 3d.hsbs.mkv", @"/movies/Iron Man/Iron Man [test].mkv" }; @@ -223,10 +225,14 @@ namespace Jellyfin.Naming.Tests.Video _namingOptions).ToList(); Assert.Single(result); - Assert.Equal(7, result[0].AlternateVersions.Count); - Assert.False(result[0].AlternateVersions[3].Is3D); - Assert.True(result[0].AlternateVersions[4].Is3D); - Assert.True(result[0].AlternateVersions[5].Is3D); + Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path); + Assert.Equal(6, result[0].AlternateVersions.Count); + Assert.Equal("/movies/Iron Man/Iron Man - 720p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/Iron Man/Iron Man - 3d.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/Iron Man/Iron Man - 3d-hsbs.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/Iron Man/Iron Man - bluray.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/Iron Man/Iron Man - test.mkv", result[0].AlternateVersions[4].Path); + Assert.Equal("/movies/Iron Man/Iron Man [test].mkv", result[0].AlternateVersions[5].Path); } [Fact] @@ -324,6 +330,33 @@ namespace Jellyfin.Naming.Tests.Video } [Fact] + public void TestMultiVersion12() + { + var files = new[] + { + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path); + Assert.Equal(5, result[0].AlternateVersions.Count); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[4].Path); + } + + [Fact] public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName() { var files = new[] diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 368c3592e..97b52f749 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -236,7 +236,7 @@ namespace Jellyfin.Naming.Tests.Video } [Fact] - public void TestFalsePositive() + public void TestMissingParttype() { var files = new[] { @@ -248,9 +248,8 @@ namespace Jellyfin.Naming.Tests.Video var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); - Assert.Single(result); - - TestStackInfo(result[0], "300", 3); + // There should be no stack, because all files should be treated as separate movies + Assert.Empty(result); } [Fact] @@ -297,11 +296,11 @@ namespace Jellyfin.Naming.Tests.Video var result = StackResolver.ResolveFiles(files, _namingOptions).ToList(); - Assert.Equal(3, result.Count); + // Only 'Bad Boys (2006)' and '300 (2006)' should be in the stack + Assert.Equal(2, result.Count); TestStackInfo(result[0], "300 (2006)", 4); - TestStackInfo(result[1], "300", 3); - TestStackInfo(result[2], "Bad Boys (2006)", 4); + TestStackInfo(result[1], "Bad Boys (2006)", 4); } [Fact] diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index cc9cfdd7d..0316377d4 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -332,7 +332,9 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), _namingOptions).ToList(); - Assert.Single(result); + // The result should contain two individual movies + // Version grouping should not work here, because the files are not in a directory with the name 'Four Sisters and a Wedding' + Assert.Equal(2, result.Count); } [Fact] diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 8ec0262bd..4b4bdd2a5 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -5,33 +5,16 @@ <ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="FsCheck.Xunit" Version="2.16.5" /> - <PackageReference Include="Moq" Version="4.18.2" /> - </ItemGroup> - - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="FsCheck.Xunit" /> + <PackageReference Include="Moq" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 61f913252..df2a2ca70 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -45,6 +45,7 @@ namespace Jellyfin.Networking.Tests [InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0::/56", "192.168.10.60")] + [InlineData("2001:abcd:abcd:6b40::0/60", "192.168.10.60")] public void InNetwork_False_Success(string network, string value) { var ip = IPAddress.Parse(value); diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 166bc0513..8174632bb 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -332,7 +332,7 @@ namespace Jellyfin.Networking.Tests // Check to see if dns resolution is working. If not, skip test. _ = IPHost.TryParse(source, out var host); - if (resultObj != null && host?.HasAddress == true) + if (resultObj is not null && host?.HasAddress == true) { result = ((IPNetAddress)resultObj[0]).ToString(true); var intf = nm.GetBindInterface(source, out _); @@ -390,7 +390,7 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; - if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj != null) + if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj is not null) { // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). result = ((IPNetAddress)resultObj[0]).ToString(true); diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 194229737..c12f0cd68 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -1,11 +1,5 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <None Include="Test Data\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -13,30 +7,19 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2"> + <PackageReference Include="coverlet.collector"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" /> </ItemGroup> diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 08b343cd8..925e8fa19 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Providers.Tests.Manager public void MergeImages_EmptyItemNewImagesEmpty_NoChange() { var itemImageProvider = GetItemImageProvider(null, null); - var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>()); + var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>(), new ImageRefreshOptions(Mock.Of<IDirectoryService>())); Assert.False(changed); } @@ -108,7 +108,7 @@ namespace Jellyfin.Providers.Tests.Manager var images = GetImages(imageType, imageCount, false); var itemImageProvider = GetItemImageProvider(null, null); - var changed = itemImageProvider.MergeImages(item, images); + var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>())); Assert.True(changed); // adds for types that allow multiple, replaces singular type images @@ -151,7 +151,7 @@ namespace Jellyfin.Providers.Tests.Manager var images = GetImages(imageType, imageCount, true); var itemImageProvider = GetItemImageProvider(null, fileSystem); - var changed = itemImageProvider.MergeImages(item, images); + var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>())); if (updateTime) { diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs index 28b2e1d8f..ec4df9981 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs @@ -91,7 +91,7 @@ namespace Jellyfin.Providers.Tests.Manager // Use type Series to hit DisplayOrder Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, oldValue, newValue, null, false, out _)); - if (lockField != null) + if (lockField is not null) { Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, oldValue, newValue, lockField, true, out _)); Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, null, newValue, lockField, false, out _)); @@ -120,7 +120,7 @@ namespace Jellyfin.Providers.Tests.Manager // Use type Audio to hit AlbumArtists Assert.False(TestMergeBaseItemData<Audio, SongInfo>(propName, oldValue, newValue, null, false, out _)); - if (lockField != null) + if (lockField is not null) { Assert.False(TestMergeBaseItemData<Audio, SongInfo>(propName, oldValue, newValue, lockField, true, out _)); Assert.False(TestMergeBaseItemData<Audio, SongInfo>(propName, Array.Empty<string>(), newValue, lockField, false, out _)); @@ -238,9 +238,6 @@ namespace Jellyfin.Providers.Tests.Manager } }; - object? result; - List<PersonInfo> actual; - // overwrite provider id var overwriteNewValue = new List<PersonInfo> { @@ -249,9 +246,9 @@ namespace Jellyfin.Providers.Tests.Manager Name = "Name 2" } }; - Assert.False(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, null, false, out result)); + Assert.False(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, null, false, out var result)); // People not already in target are not merged into it from source - actual = (List<PersonInfo>)result!; + List<PersonInfo> actual = (List<PersonInfo>)result!; Assert.Single(actual); Assert.Equal("Name 1", actual[0].Name); @@ -328,11 +325,11 @@ namespace Jellyfin.Providers.Tests.Manager People = oldValue }; - var lockedFields = lockField == null ? Array.Empty<MetadataField>() : new[] { (MetadataField)lockField }; + var lockedFields = lockField is null ? Array.Empty<MetadataField>() : new[] { (MetadataField)lockField }; MetadataService<Movie, MovieInfo>.MergeBaseItemData(source, target, lockedFields, replaceData, false); actualValue = target.People; - return newValue?.Equals(actualValue) ?? actualValue == null; + return newValue?.Equals(actualValue) ?? actualValue is null; } /// <summary> @@ -367,12 +364,12 @@ namespace Jellyfin.Providers.Tests.Manager }; property.SetValue(target.Item, oldValue); - var lockedFields = lockField == null ? Array.Empty<MetadataField>() : new[] { (MetadataField)lockField }; + var lockedFields = lockField is null ? Array.Empty<MetadataField>() : new[] { (MetadataField)lockField }; // generic type doesn't actually matter to call the static method, just has to be filled in MetadataService<TItemType, TIdType>.MergeBaseItemData(source, target, lockedFields, replaceData, false); actualValue = property.GetValue(target.Item); - return newValue?.Equals(actualValue) ?? actualValue == null; + return newValue?.Equals(actualValue) ?? actualValue is null; } } } diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs new file mode 100644 index 000000000..400e30bd6 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -0,0 +1,614 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.BaseItemManager; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Providers.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +// Allow Moq to see internal class +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + +namespace Jellyfin.Providers.Tests.Manager +{ + public class ProviderManagerTests + { + private static readonly ILogger<ProviderManager> _logger = new NullLogger<ProviderManager>(); + + public static TheoryData<Mock<IMetadataService>[], int> RefreshSingleItemOrderData() + => new() + { + // no order set, uses provided order + { + new[] + { + MockIMetadataService(true, true), + MockIMetadataService(true, true) + }, + 0 + }, + // sort order sets priority when all match + { + new[] + { + MockIMetadataService(true, true, 1), + MockIMetadataService(true, true, 0), + MockIMetadataService(true, true, 2) + }, + 1 + }, + // CanRefreshPrimary prioritized + { + new[] + { + MockIMetadataService(false, true), + MockIMetadataService(true, true), + }, + 1 + }, + // falls back to CanRefresh + { + new[] + { + MockIMetadataService(false, false), + MockIMetadataService(false, true) + }, + 1 + }, + }; + + [Theory] + [MemberData(nameof(RefreshSingleItemOrderData))] + public async Task RefreshSingleItem_ServiceOrdering_FollowsPriority(Mock<IMetadataService>[] servicesList, int expectedIndex) + { + var item = new Movie(); + + using var providerManager = GetProviderManager(); + AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray()); + + var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); + var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false); + + Assert.Equal(ItemUpdateType.MetadataDownload, actual); + for (var i = 0; i < servicesList.Length; i++) + { + var times = i == expectedIndex ? Times.Once() : Times.Never(); + servicesList[i].Verify(mock => mock.RefreshMetadata(It.IsAny<BaseItem>(), It.IsAny<MetadataRefreshOptions>(), It.IsAny<CancellationToken>()), times); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RefreshSingleItem_RefreshMetadata_WhenServiceFound(bool serviceFound) + { + var item = new Movie(); + + var servicesList = new[] { MockIMetadataService(false, serviceFound) }; + + using var providerManager = GetProviderManager(); + AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray()); + + var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); + var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false); + + var expectedResult = serviceFound ? ItemUpdateType.MetadataDownload : ItemUpdateType.None; + Assert.Equal(expectedResult, actual); + } + + public static TheoryData<int, int[]?, int[]?, int?[]?, int[]> GetImageProvidersOrderData() + => new() + { + { 3, null, null, null, new[] { 0, 1, 2 } }, // no order options set + + // library options ordering + { 3, Array.Empty<int>(), null, null, new[] { 0, 1, 2 } }, // no order provided + { 3, new[] { 1 }, null, null, new[] { 1, 0, 2 } }, // one item in order + { 3, new[] { 2, 1, 0 }, null, null, new[] { 2, 1, 0 } }, // full reverse order + + // server options ordering + { 3, null, Array.Empty<int>(), null, new[] { 0, 1, 2 } }, // no order provided + { 3, null, new[] { 1 }, null, new[] { 1, 0, 2 } }, // one item in order + { 3, null, new[] { 2, 1, 0 }, null, new[] { 2, 1, 0 } }, // full reverse order + + // IHasOrder ordering + { 3, null, null, new int?[] { null, 1, null }, new[] { 1, 0, 2 } }, // one item with defined order + { 3, null, null, new int?[] { 2, 1, 0 }, new[] { 2, 1, 0 } }, // full reverse order + + // multiple orders set + { 3, new[] { 1 }, new[] { 2, 0, 1 }, null, new[] { 1, 0, 2 } }, // partial library order first, server order ignored + { 3, new[] { 1 }, null, new int?[] { 2, 0, 1 }, new[] { 1, 2, 0 } }, // library order first, then orderby + { 3, new[] { 2, 1, 0 }, new[] { 1, 2, 0 }, new int?[] { 2, 0, 1 }, new[] { 2, 1, 0 } }, // library order wins + }; + + [Theory] + [MemberData(nameof(GetImageProvidersOrderData))] + public void GetImageProviders_ProviderOrder_MatchesExpected(int providerCount, int[]? libraryOrder, int[]? serverOrder, int?[]? hasOrderOrder, int[] expectedOrder) + { + var item = new Movie(); + + var nameProvider = new Func<int, string>(i => "Provider" + i); + + var providerList = new List<IImageProvider>(); + for (var i = 0; i < providerCount; i++) + { + var order = hasOrderOrder?[i]; + providerList.Add(MockIImageProvider<ILocalImageProvider>(nameProvider(i), item, order: order)); + } + + var libraryOptions = CreateLibraryOptions(item.GetType().Name, imageFetcherOrder: libraryOrder?.Select(nameProvider).ToArray()); + var serverConfiguration = CreateServerConfiguration(item.GetType().Name, imageFetcherOrder: serverOrder?.Select(nameProvider).ToArray()); + + using var providerManager = GetProviderManager(serverConfiguration: serverConfiguration, libraryOptions: libraryOptions); + AddParts(providerManager, imageProviders: providerList); + + var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); + var actualProviders = providerManager.GetImageProviders(item, refreshOptions).ToList(); + + Assert.Equal(providerList.Count, actualProviders.Count); + var actualOrder = actualProviders.Select(i => providerList.IndexOf(i)).ToArray(); + Assert.Equal(expectedOrder, actualOrder); + } + + [Theory] + [InlineData(true, false, true)] + [InlineData(false, false, false)] + [InlineData(true, true, false)] + public void GetImageProviders_CanRefreshImagesBasic_WhenSupportsWithoutError(bool supports, bool errorOnSupported, bool expected) + { + GetImageProviders_CanRefreshImages_Tester(nameof(IImageProvider), supports, expected, errorOnSupported: errorOnSupported); + } + + [Theory] + [InlineData(nameof(ILocalImageProvider), false, true)] + [InlineData(nameof(ILocalImageProvider), true, true)] + [InlineData(nameof(IImageProvider), false, false)] + [InlineData(nameof(IImageProvider), true, true)] + public void GetImageProviders_CanRefreshImagesLocked_WhenLocalOrFullRefresh(string providerType, bool fullRefresh, bool expected) + { + GetImageProviders_CanRefreshImages_Tester(providerType, true, expected, itemLocked: true, fullRefresh: fullRefresh); + } + + [Theory] + [InlineData(nameof(ILocalImageProvider), false, true)] + [InlineData(nameof(IRemoteImageProvider), true, true)] + [InlineData(nameof(IDynamicImageProvider), true, true)] + [InlineData(nameof(IRemoteImageProvider), false, false)] + [InlineData(nameof(IDynamicImageProvider), false, false)] + public void GetImageProviders_CanRefreshImagesBaseItemEnabled_WhenLocalOrEnabled(string providerType, bool enabled, bool expected) + { + GetImageProviders_CanRefreshImages_Tester(providerType, true, expected, baseItemEnabled: enabled); + } + + private static void GetImageProviders_CanRefreshImages_Tester( + string providerType, + bool supports, + bool expected, + bool errorOnSupported = false, + bool itemLocked = false, + bool fullRefresh = false, + bool baseItemEnabled = true) + { + var item = new Movie + { + IsLocked = itemLocked + }; + + var providerName = "provider"; + IImageProvider provider = providerType switch + { + "IImageProvider" => MockIImageProvider<IImageProvider>(providerName, item, supports: supports, errorOnSupported: errorOnSupported), + "ILocalImageProvider" => MockIImageProvider<ILocalImageProvider>(providerName, item, supports: supports, errorOnSupported: errorOnSupported), + "IRemoteImageProvider" => MockIImageProvider<IRemoteImageProvider>(providerName, item, supports: supports, errorOnSupported: errorOnSupported), + "IDynamicImageProvider" => MockIImageProvider<IDynamicImageProvider>(providerName, item, supports: supports, errorOnSupported: errorOnSupported), + _ => throw new ArgumentException("Unexpected provider type") + }; + + var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)) + { + ImageRefreshMode = fullRefresh ? MetadataRefreshMode.FullRefresh : MetadataRefreshMode.Default + }; + + var baseItemManager = new Mock<IBaseItemManager>(MockBehavior.Strict); + baseItemManager.Setup(i => i.IsImageFetcherEnabled(item, It.IsAny<TypeOptions>(), providerName)) + .Returns(baseItemEnabled); + + using var providerManager = GetProviderManager(baseItemManager: baseItemManager.Object); + AddParts(providerManager, imageProviders: new[] { provider }); + + var actualProviders = providerManager.GetImageProviders(item, refreshOptions).ToArray(); + + Assert.Equal(expected ? 1 : 0, actualProviders.Length); + } + + public static TheoryData<string[], int[]?, int[]?, int[]?, int[]?, int?[]?, int[]> GetMetadataProvidersOrderData() + { + var l = nameof(ILocalMetadataProvider); + var r = nameof(IRemoteMetadataProvider); + return new() + { + { new[] { l, l, r, r }, null, null, null, null, null, new[] { 0, 1, 2, 3 } }, // no order options set + + // library options ordering + { new[] { l, l, r, r }, Array.Empty<int>(), Array.Empty<int>(), null, null, null, new[] { 0, 1, 2, 3 } }, // no order provided + // local only + { new[] { r, l, l, l }, new[] { 2 }, null, null, null, null, new[] { 2, 0, 1, 3 } }, // one item in order + { new[] { r, l, l, l }, new[] { 3, 2, 1 }, null, null, null, null, new[] { 3, 2, 1, 0 } }, // full reverse order + // remote only + { new[] { l, r, r, r }, null, new[] { 2 }, null, null, null, new[] { 2, 0, 1, 3 } }, // one item in order + { new[] { l, r, r, r }, null, new[] { 3, 2, 1 }, null, null, null, new[] { 3, 2, 1, 0 } }, // full reverse order + // local and remote, note that results will be interleaved (odd but expected) + { new[] { l, l, r, r }, new[] { 1 }, new[] { 3 }, null, null, null, new[] { 1, 3, 0, 2 } }, // one item in each order + { new[] { l, l, l, r, r, r }, new[] { 2, 1, 0 }, new[] { 5, 4, 3 }, null, null, null, new[] { 2, 5, 1, 4, 0, 3 } }, // full reverse order + + // // server options ordering + { new[] { l, l, r, r }, null, null, Array.Empty<int>(), Array.Empty<int>(), null, new[] { 0, 1, 2, 3 } }, // no order provided + // local only + { new[] { r, l, l, l }, null, null, new[] { 2 }, null, null, new[] { 2, 0, 1, 3 } }, // one item in order + { new[] { r, l, l, l }, null, null, new[] { 3, 2, 1 }, null, null, new[] { 3, 2, 1, 0 } }, // full reverse order + // remote only + { new[] { l, r, r, r }, null, null, null, new[] { 2 }, null, new[] { 2, 0, 1, 3 } }, // one item in order + { new[] { l, r, r, r }, null, null, null, new[] { 3, 2, 1 }, null, new[] { 3, 2, 1, 0 } }, // full reverse order + // local and remote, note that results will be interleaved (odd but expected) + { new[] { l, l, r, r }, null, null, new[] { 1 }, new[] { 3 }, null, new[] { 1, 3, 0, 2 } }, // one item in each order + { new[] { l, l, l, r, r, r }, null, null, new[] { 2, 1, 0 }, new[] { 5, 4, 3 }, null, new[] { 2, 5, 1, 4, 0, 3 } }, // full reverse order + + // IHasOrder ordering (not interleaved, doesn't care about types) + { new[] { l, l, r, r }, null, null, null, null, new int?[] { 2, null, 1, null }, new[] { 2, 0, 1, 3 } }, // partially defined + { new[] { l, l, r, r }, null, null, null, null, new int?[] { 3, 2, 1, 0 }, new[] { 3, 2, 1, 0 } }, // full reverse order + + // multiple orders set + { new[] { l, l, l, r, r, r }, new[] { 1 }, new[] { 4 }, new[] { 2, 1, 0 }, new[] { 5, 4, 3 }, null, new[] { 1, 4, 0, 2, 3, 5 } }, // partial library order first, server order ignored + { new[] { l, l, l }, new[] { 1 }, null, null, null, new int?[] { 2, 0, 1 }, new[] { 1, 2, 0 } }, // library order first, then orderby + { new[] { l, l, l, r, r, r }, new[] { 2, 1, 0 }, new[] { 5, 4, 3 }, new[] { 1, 2, 0 }, new[] { 4, 5, 3 }, new int?[] { 5, 4, 1, 6, 3, 2 }, new[] { 2, 5, 4, 1, 0, 3 } }, // library order wins (with orderby between local/remote) + }; + } + + [Theory] + [MemberData(nameof(GetMetadataProvidersOrderData))] + public void GetMetadataProviders_ProviderOrder_MatchesExpected( + string[] providers, + int[]? libraryLocalOrder, + int[]? libraryRemoteOrder, + int[]? serverLocalOrder, + int[]? serverRemoteOrder, + int?[]? hasOrderOrder, + int[] expectedOrder) + { + var item = new MetadataTestItem(); + + var nameProvider = new Func<int, string>(i => "Provider" + i); + + var providerList = new List<IMetadataProvider<MetadataTestItem>>(); + for (var i = 0; i < providers.Length; i++) + { + var order = hasOrderOrder?[i]; + providerList.Add(MockIMetadataProviderMapper<MetadataTestItem, MetadataTestItemInfo>(providers[i], nameProvider(i), order: order)); + } + + var libraryOptions = CreateLibraryOptions( + item.GetType().Name, + localMetadataReaderOrder: libraryLocalOrder?.Select(nameProvider).ToArray(), + metadataFetcherOrder: libraryRemoteOrder?.Select(nameProvider).ToArray()); + var serverConfiguration = CreateServerConfiguration( + item.GetType().Name, + localMetadataReaderOrder: serverLocalOrder?.Select(nameProvider).ToArray(), + metadataFetcherOrder: serverRemoteOrder?.Select(nameProvider).ToArray()); + + var baseItemManager = new Mock<IBaseItemManager>(MockBehavior.Strict); + baseItemManager.Setup(i => i.IsMetadataFetcherEnabled(item, It.IsAny<TypeOptions>(), It.IsAny<string>())) + .Returns(true); + + using var providerManager = GetProviderManager(serverConfiguration: serverConfiguration, baseItemManager: baseItemManager.Object); + AddParts(providerManager, metadataProviders: providerList); + + var actualProviders = providerManager.GetMetadataProviders<MetadataTestItem>(item, libraryOptions).ToList(); + + Assert.Equal(providerList.Count, actualProviders.Count); + var actualOrder = actualProviders.Select(i => providerList.IndexOf(i)).ToArray(); + Assert.Equal(expectedOrder, actualOrder); + } + + [Theory] + [InlineData(nameof(IMetadataProvider))] + [InlineData(nameof(ILocalMetadataProvider))] + [InlineData(nameof(IRemoteMetadataProvider))] + [InlineData(nameof(ICustomMetadataProvider))] + public void GetMetadataProviders_CanRefreshMetadataBasic_ReturnsTrue(string providerType) + { + GetMetadataProviders_CanRefreshMetadata_Tester(providerType, true); + } + + [Theory] + [InlineData(nameof(ILocalMetadataProvider), false, true)] + [InlineData(nameof(IRemoteMetadataProvider), false, false)] + [InlineData(nameof(ICustomMetadataProvider), false, false)] + [InlineData(nameof(ILocalMetadataProvider), true, true)] + [InlineData(nameof(ICustomMetadataProvider), true, false)] + public void GetMetadataProviders_CanRefreshMetadataLocked_WhenLocalOrForced(string providerType, bool forced, bool expected) + { + GetMetadataProviders_CanRefreshMetadata_Tester(providerType, expected, itemLocked: true, providerForced: forced); + } + + [Theory] + [InlineData(nameof(ILocalMetadataProvider), false, true)] + [InlineData(nameof(ICustomMetadataProvider), false, true)] + [InlineData(nameof(IRemoteMetadataProvider), false, false)] + [InlineData(nameof(IRemoteMetadataProvider), true, true)] + public void GetMetadataProviders_CanRefreshMetadataBaseItemEnabled_WhenEnabledOrNotRemote(string providerType, bool baseItemEnabled, bool expected) + { + GetMetadataProviders_CanRefreshMetadata_Tester(providerType, expected, baseItemEnabled: baseItemEnabled); + } + + [Theory] + [InlineData(nameof(IRemoteMetadataProvider), false, true)] + [InlineData(nameof(ICustomMetadataProvider), false, true)] + [InlineData(nameof(ILocalMetadataProvider), false, false)] + [InlineData(nameof(ILocalMetadataProvider), true, true)] + public void GetMetadataProviders_CanRefreshMetadataSupportsLocal_WhenSupportsOrNotLocal(string providerType, bool supportsLocalMetadata, bool expected) + { + GetMetadataProviders_CanRefreshMetadata_Tester(providerType, expected, supportsLocalMetadata: supportsLocalMetadata); + } + + [Theory] + [InlineData(nameof(ICustomMetadataProvider), true)] + [InlineData(nameof(IRemoteMetadataProvider), true)] + [InlineData(nameof(ILocalMetadataProvider), true)] + public void GetMetadataProviders_CanRefreshMetadataOwned(string providerType, bool expected) + { + GetMetadataProviders_CanRefreshMetadata_Tester(providerType, expected, ownedItem: true); + } + + private static void GetMetadataProviders_CanRefreshMetadata_Tester( + string providerType, + bool expected, + bool itemLocked = false, + bool baseItemEnabled = true, + bool providerForced = false, + bool supportsLocalMetadata = true, + bool ownedItem = false) + { + var item = new MetadataTestItem + { + IsLocked = itemLocked, + OwnerId = ownedItem ? Guid.NewGuid() : Guid.Empty, + EnableLocalMetadata = supportsLocalMetadata + }; + + var providerName = "provider"; + var provider = MockIMetadataProviderMapper<MetadataTestItem, MetadataTestItemInfo>(providerType, providerName, forced: providerForced); + + var baseItemManager = new Mock<IBaseItemManager>(MockBehavior.Strict); + baseItemManager.Setup(i => i.IsMetadataFetcherEnabled(item, It.IsAny<TypeOptions>(), providerName)) + .Returns(baseItemEnabled); + + using var providerManager = GetProviderManager(baseItemManager: baseItemManager.Object); + AddParts(providerManager, metadataProviders: new[] { provider }); + + var actualProviders = providerManager.GetMetadataProviders<MetadataTestItem>(item, new LibraryOptions()).ToArray(); + + Assert.Equal(expected ? 1 : 0, actualProviders.Length); + } + + private static Mock<IMetadataService> MockIMetadataService(bool refreshPrimary, bool canRefresh, int order = 0) + { + var service = new Mock<IMetadataService>(MockBehavior.Strict); + service.Setup(s => s.Order) + .Returns(order); + service.Setup(s => s.CanRefreshPrimary(It.IsAny<Type>())) + .Returns(refreshPrimary); + service.Setup(s => s.CanRefresh(It.IsAny<BaseItem>())) + .Returns(canRefresh); + service.Setup(s => s.RefreshMetadata(It.IsAny<BaseItem>(), It.IsAny<MetadataRefreshOptions>(), It.IsAny<CancellationToken>())) + .Returns(Task.FromResult(ItemUpdateType.MetadataDownload)); + return service; + } + + private static IImageProvider MockIImageProvider<TProviderType>(string name, BaseItem expectedType, bool supports = true, int? order = null, bool errorOnSupported = false) + where TProviderType : class, IImageProvider + { + Mock<IHasOrder>? hasOrder = null; + if (order is not null) + { + hasOrder = new Mock<IHasOrder>(MockBehavior.Strict); + hasOrder.Setup(i => i.Order) + .Returns((int)order); + } + + var provider = hasOrder is null + ? new Mock<TProviderType>(MockBehavior.Strict) + : hasOrder.As<TProviderType>(); + provider.Setup(p => p.Name) + .Returns(name); + if (errorOnSupported) + { + provider.Setup(p => p.Supports(It.IsAny<BaseItem>())) + .Throws(new ArgumentException("Provider threw exception on Supports(item)")); + } + else + { + provider.Setup(p => p.Supports(expectedType)) + .Returns(supports); + } + + return provider.Object; + } + + private static IMetadataProvider<TItemType> MockIMetadataProviderMapper<TItemType, TLookupInfoType>(string typeName, string providerName, int? order = null, bool forced = false) + where TItemType : BaseItem, IHasLookupInfo<TLookupInfoType> + where TLookupInfoType : ItemLookupInfo, new() + => typeName switch + { + "ILocalMetadataProvider" => MockIMetadataProvider<ILocalMetadataProvider<TItemType>, TItemType>(providerName, order, forced), + "IRemoteMetadataProvider" => MockIMetadataProvider<IRemoteMetadataProvider<TItemType, TLookupInfoType>, TItemType>(providerName, order, forced), + "ICustomMetadataProvider" => MockIMetadataProvider<ICustomMetadataProvider<TItemType>, TItemType>(providerName, order, forced), + _ => MockIMetadataProvider<IMetadataProvider<TItemType>, TItemType>(providerName, order, forced) + }; + + private static IMetadataProvider<TItemType> MockIMetadataProvider<TProviderType, TItemType>(string name, int? order = null, bool forced = false) + where TProviderType : class, IMetadataProvider<TItemType> + where TItemType : BaseItem + { + Mock<IForcedProvider>? forcedProvider = null; + if (forced) + { + forcedProvider = new Mock<IForcedProvider>(); + } + + Mock<IHasOrder>? hasOrder = null; + if (order is not null) + { + hasOrder = forcedProvider is null ? new Mock<IHasOrder>() : forcedProvider.As<IHasOrder>(); + hasOrder.Setup(i => i.Order) + .Returns((int)order); + } + + var provider = hasOrder is null + ? new Mock<TProviderType>(MockBehavior.Strict) + : hasOrder.As<TProviderType>(); + provider.Setup(p => p.Name) + .Returns(name); + + return provider.Object; + } + + private static LibraryOptions CreateLibraryOptions( + string typeName, + string[]? imageFetcherOrder = null, + string[]? localMetadataReaderOrder = null, + string[]? metadataFetcherOrder = null) + { + var libraryOptions = new LibraryOptions + { + LocalMetadataReaderOrder = localMetadataReaderOrder + }; + + // only create type options if populating it with something + if (imageFetcherOrder is not null || metadataFetcherOrder is not null) + { + imageFetcherOrder ??= Array.Empty<string>(); + metadataFetcherOrder ??= Array.Empty<string>(); + + libraryOptions.TypeOptions = new[] + { + new TypeOptions + { + Type = typeName, + ImageFetcherOrder = imageFetcherOrder, + MetadataFetcherOrder = metadataFetcherOrder + } + }; + } + + return libraryOptions; + } + + private static ServerConfiguration CreateServerConfiguration( + string typeName, + string[]? imageFetcherOrder = null, + string[]? localMetadataReaderOrder = null, + string[]? metadataFetcherOrder = null) + { + var serverConfiguration = new ServerConfiguration(); + + // only create type options if populating it with something + if (imageFetcherOrder is not null || localMetadataReaderOrder is not null || metadataFetcherOrder is not null) + { + imageFetcherOrder ??= Array.Empty<string>(); + localMetadataReaderOrder ??= Array.Empty<string>(); + metadataFetcherOrder ??= Array.Empty<string>(); + + serverConfiguration.MetadataOptions = new[] + { + new MetadataOptions + { + ItemType = typeName, + ImageFetcherOrder = imageFetcherOrder, + LocalMetadataReaderOrder = localMetadataReaderOrder, + MetadataFetcherOrder = metadataFetcherOrder + } + }; + } + + return serverConfiguration; + } + + private static ProviderManager GetProviderManager( + ServerConfiguration? serverConfiguration = null, + LibraryOptions? libraryOptions = null, + IBaseItemManager? baseItemManager = null) + { + var serverConfigurationManager = new Mock<IServerConfigurationManager>(MockBehavior.Strict); + serverConfigurationManager.Setup(i => i.Configuration) + .Returns(serverConfiguration ?? new ServerConfiguration()); + + var libraryManager = new Mock<ILibraryManager>(MockBehavior.Strict); + libraryManager.Setup(i => i.GetLibraryOptions(It.IsAny<BaseItem>())) + .Returns(libraryOptions ?? new LibraryOptions()); + + var providerManager = new ProviderManager( + Mock.Of<IHttpClientFactory>(), + Mock.Of<ISubtitleManager>(), + serverConfigurationManager.Object, + Mock.Of<ILibraryMonitor>(), + _logger, + Mock.Of<IFileSystem>(), + Mock.Of<IServerApplicationPaths>(), + libraryManager.Object, + baseItemManager!); + + return providerManager; + } + + private static void AddParts( + ProviderManager providerManager, + IEnumerable<IImageProvider>? imageProviders = null, + IEnumerable<IMetadataService>? metadataServices = null, + IEnumerable<IMetadataProvider>? metadataProviders = null, + IEnumerable<IMetadataSaver>? metadataSavers = null, + IEnumerable<IExternalId>? externalIds = null) + { + imageProviders ??= Array.Empty<IImageProvider>(); + metadataServices ??= Array.Empty<IMetadataService>(); + metadataProviders ??= Array.Empty<IMetadataProvider>(); + metadataSavers ??= Array.Empty<IMetadataSaver>(); + externalIds ??= Array.Empty<IExternalId>(); + + providerManager.AddParts(imageProviders, metadataServices, metadataProviders, metadataSavers, externalIds); + } + + /// <summary> + /// Simple <see cref="BaseItem"/> extension to make SupportsLocalMetadata directly settable. + /// </summary> + internal class MetadataTestItem : BaseItem, IHasLookupInfo<MetadataTestItemInfo> + { + public bool EnableLocalMetadata { get; set; } = true; + + public override bool SupportsLocalMetadata => EnableLocalMetadata; + + public MetadataTestItemInfo GetLookupInfo() + { + return GetItemLookupInfo<MetadataTestItemInfo>(); + } + } + + internal class MetadataTestItemInfo : ItemLookupInfo + { + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index 9b80f0b94..6b2d9021c 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -80,7 +80,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None); Assert.NotNull(actual); - if (expectedFormat == null) + if (expectedFormat is null) { Assert.False(actual.HasImage); } @@ -131,7 +131,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None); Assert.NotNull(actual); - if (expectedFormat == null) + if (expectedFormat is null) { Assert.False(actual.HasImage); } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs index 6b2a05241..7ea6f7d9c 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs @@ -110,7 +110,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo private static IMediaSourceManager GetMediaSourceManager(Video item, MediaStream? defaultStream, List<MediaStream> mediaStreams) { var defaultStreamList = new List<MediaStream>(); - if (defaultStream != null) + if (defaultStream is not null) { defaultStreamList.Add(defaultStream); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs index 4c7c56311..0d2b488bc 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -6,6 +6,7 @@ using Emby.Server.Implementations.Data; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Configuration; using Moq; using Xunit; @@ -27,8 +28,18 @@ namespace Jellyfin.Server.Implementations.Tests.Data appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>())) .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal)); + var configSection = new Mock<IConfigurationSection>(); + configSection + .SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)]) + .Returns("0"); + var config = new Mock<IConfiguration>(); + config + .Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey))) + .Returns(configSection.Object); + _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); _fixture.Inject(appHost); + _fixture.Inject(config); _sqliteItemRepository = _fixture.Create<SqliteItemRepository>(); } @@ -282,7 +293,7 @@ namespace Jellyfin.Server.Implementations.Tests.Data Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values)); } - private class ProviderIdsExtensionsTestsObject : IHasProviderIds + private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds { public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>(); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs index ef8f7cd90..f01611819 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs @@ -48,7 +48,7 @@ namespace Jellyfin.Server.Implementations.Tests.HttpServer Assert.Throws<JsonException>(() => con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed)); } - internal class BufferSegment : ReadOnlySequenceSegment<byte> + internal sealed class BufferSegment : ReadOnlySequenceSegment<byte> { public BufferSegment(Memory<byte> memory) { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 918802d77..9b6cb40b0 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -5,13 +5,6 @@ <ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid> </PropertyGroup> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace> - </PropertyGroup> - <ItemGroup> <None Include="Test Data\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -19,28 +12,17 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.17.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="Xunit.SkippableFact" /> + <PackageReference Include="coverlet.collector" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs new file mode 100644 index 000000000..d136c1bc6 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs @@ -0,0 +1,76 @@ +using System.Linq; +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.Audio; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library; + +public class AudioResolverTests +{ + private static readonly NamingOptions _namingOptions = new(); + + [Theory] + [InlineData("words.mp3")] // single non-tagged file + [InlineData("chapter 01.mp3")] + [InlineData("part 1.mp3")] + [InlineData("chapter 01.mp3", "non-media.txt")] + [InlineData("title.mp3", "title.epub")] + [InlineData("01.mp3", "subdirectory/")] // single media file with sub-directory - note that this will hide any contents in the subdirectory + public void Resolve_AudiobookDirectory_SingleResult(params string[] children) + { + var resolved = TestResolveChildren("/parent/title", children); + Assert.NotNull(resolved); + } + + [Theory] + /* Results that can't be displayed as an audio book. */ + [InlineData] // no contents + [InlineData("subdirectory/")] + [InlineData("non-media.txt")] + /* Names don't indicate parts of a single book. */ + [InlineData("Name.mp3", "Another Name.mp3")] + /* Results that are an audio book but not currently navigable as such (multiple chapters and/or parts). */ + [InlineData("01.mp3", "02.mp3")] + [InlineData("chapter 01.mp3", "chapter 02.mp3")] + [InlineData("part 1.mp3", "part 2.mp3")] + [InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")] + /* Mismatched chapters, parts, and named files. */ + [InlineData("chapter 01.mp3", "part 2.mp3")] + [InlineData("book title.mp3", "chapter name.mp3")] // "book title" resolves as alternate version of book based on directory name + [InlineData("01 Content.mp3", "01 Credits.mp3")] // resolves as alternate versions of chapter 1 + [InlineData("Chapter Name.mp3", "Part 1.mp3")] + public void Resolve_AudiobookDirectory_NoResult(params string[] children) + { + var resolved = TestResolveChildren("/parent/book title", children); + Assert.Null(resolved); + } + + private Audio? TestResolveChildren(string parent, string[] children) + { + var childrenMetadata = children.Select(name => new FileSystemMetadata + { + FullName = parent + "/" + name, + IsDirectory = name.EndsWith('/') + }).ToArray(); + + var resolver = new AudioResolver(_namingOptions); + var itemResolveArgs = new ItemResolveArgs( + null, + Mock.Of<ILibraryManager>()) + { + CollectionType = "books", + FileInfo = new FileSystemMetadata + { + FullName = parent, + IsDirectory = true + }, + FileSystemChildren = childrenMetadata + }; + + return resolver.Resolve(itemResolveArgs); + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index c21871297..6d0ed7bbb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -22,10 +22,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library { var parent = new Folder { Name = "extras" }; - var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); + var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>()) + null) { Parent = parent, CollectionType = CollectionType.TvShows, @@ -45,10 +45,10 @@ namespace Jellyfin.Server.Implementations.Tests.Library // Have to create a mock because of moq proxies not being castable to a concrete implementation // https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48 - var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions); + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>()) + null) { Parent = series, CollectionType = CollectionType.TvShows, @@ -60,9 +60,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.NotNull(episodeResolver.Resolve(itemResolveArgs)); } - private class EpisodeResolverMock : EpisodeResolver + private sealed class EpisodeResolverMock : EpisodeResolver { - public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) : base(logger, namingOptions) + public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService) { } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs index 599599071..562711337 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs @@ -80,6 +80,35 @@ public class FindExtrasTests } [Fact] + public void FindExtras_SeparateMovieFolder_CleanExtraNames() + { + var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" }; + var paths = new List<string> + { + "/movies/Up/Up.mkv", + "/movies/Up/Recording the audio[Bluray]-behindthescenes.mkv", + "/movies/Up/Interview with the dog-interview.mkv", + "/movies/Up/shorts/Balloons[1080p].mkv" + }; + + var files = paths.Select(p => new FileSystemMetadata + { + FullName = p, + IsDirectory = false + }).ToList(); + + var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList(); + + Assert.Equal(3, extras.Count); + Assert.Equal(ExtraType.BehindTheScenes, extras[0].ExtraType); + Assert.Equal("Recording the audio", extras[0].Name); + Assert.Equal(ExtraType.Interview, extras[1].ExtraType); + Assert.Equal("Interview with the dog", extras[1].Name); + Assert.Equal(ExtraType.Short, extras[2].ExtraType); + Assert.Equal("Balloons", extras[2].Name); + } + + [Fact] public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsCorrectExtras() { var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" }; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs index 538010f6c..07feae587 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs @@ -51,4 +51,68 @@ public class MediaStreamSelectorTests Assert.Equal(expectedIndex, MediaStreamSelector.GetDefaultAudioStreamIndex(streams, preferredLanguages, preferDefaultTrack)); } + + public static TheoryData<MediaStream, int> GetStreamScore_MediaStream_TestData() + { + var data = new TheoryData<MediaStream, int>(); + + data.Add(new MediaStream(), 111111); + data.Add( + new MediaStream() + { + Language = "eng" + }, + 10111111); + data.Add( + new MediaStream() + { + Language = "fre" + }, + 10011111); + data.Add( + new MediaStream() + { + IsForced = true + }, + 121111); + data.Add( + new MediaStream() + { + IsDefault = true + }, + 112111); + data.Add( + new MediaStream() + { + SupportsExternalStream = true + }, + 111211); + data.Add( + new MediaStream() + { + IsExternal = true + }, + 111112); + data.Add( + new MediaStream() + { + Language = "eng", + IsForced = true, + IsDefault = true, + SupportsExternalStream = true, + IsExternal = true + }, + 10122212); + + return data; + } + + [Theory] + [MemberData(nameof(GetStreamScore_MediaStream_TestData))] + public void GetStreamScore_MediaStream_CorrectScore(MediaStream stream, int expectedScore) + { + var languagePref = new[] { "eng", "fre" }; + + Assert.Equal(expectedScore, MediaStreamSelector.GetStreamScore(stream, languagePref)); + } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs index efc3ac0c2..aed584355 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs @@ -18,10 +18,10 @@ public class MovieResolverTests [Fact] public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo() { - var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions); + var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); var itemResolveArgs = new ItemResolveArgs( Mock.Of<IServerApplicationPaths>(), - Mock.Of<IDirectoryService>()) + null) { Parent = null, FileInfo = new FileSystemMetadata diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index be2dfe0a8..c33a957e6 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Emby.Server.Implementations.Library; using Xunit; @@ -73,5 +74,47 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result)); Assert.Null(result); } + + [Theory] + [InlineData(null, '/', null)] + [InlineData(null, '\\', null)] + [InlineData("/home/jeff/myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")] + [InlineData("C:\\Users\\Jeff\\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")] + [InlineData("\\home/jeff\\myfile.mkv", '\\', "\\home\\jeff\\myfile.mkv")] + [InlineData("\\home/jeff\\myfile.mkv", '/', "/home/jeff/myfile.mkv")] + [InlineData("", '/', "")] + public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath) + { + Assert.Equal(expectedPath, path.NormalizePath(separator)); + } + + [Theory] + [InlineData("/home/jeff/myfile.mkv")] + [InlineData("C:\\Users\\Jeff\\myfile.mkv")] + [InlineData("\\home/jeff\\myfile.mkv")] + public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path) + { + var separator = Path.DirectorySeparatorChar; + + Assert.Equal(path.Replace('\\', separator).Replace('/', separator), path.NormalizePath()); + } + + [Theory] + [InlineData("/home/jeff/myfile.mkv", '/')] + [InlineData("C:\\Users\\Jeff\\myfile.mkv", '\\')] + [InlineData("\\home/jeff\\myfile.mkv", '/')] + public void NormalizePath_OutVar_Correct(string path, char expectedSeparator) + { + var result = path.NormalizePath(out var separator); + + Assert.Equal(expectedSeparator, separator); + Assert.Equal(path.Replace('\\', separator).Replace('/', separator), result); + } + + [Fact] + public void NormalizePath_SpecifyInvalidSeparator_ThrowsException() + { + Assert.Throws<ArgumentException>(() => string.Empty.NormalizePath('a')); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs index 82ce8fc4e..92b4178fd 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs @@ -67,4 +67,23 @@ public class XmlTvListingsProviderTests Assert.Equal("https://domain.tld/image.png", program.ImageUrl); Assert.Equal("3297", program.ChannelId); } + + [Theory] + [InlineData("Test Data/LiveTv/Listings/XmlTv/emptycategory.xml")] + [InlineData("https://example.com/emptycategory.xml")] + public async Task GetProgramsAsync_EmptyCategories_Success(string path) + { + var info = new ListingsProviderInfo() + { + Path = path + }; + + var startDate = new DateTime(2022, 11, 4); + var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None); + var programsList = programs.ToList(); + Assert.Single(programsList); + var program = programsList[0]; + Assert.DoesNotContain(program.Genres, g => string.IsNullOrEmpty(g)); + Assert.Equal("3297", program.ChannelId); + } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 3e7d6ed1d..7fabe9904 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var cultures = localizationManager.GetCultures().ToList(); - Assert.Equal(190, cultures.Count); + Assert.Equal(191, cultures.Count); var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal)); Assert.NotNull(germany); @@ -83,11 +83,11 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(23, ratings.Count); + Assert.Equal(54, ratings.Count); var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal)); Assert.NotNull(tvma); - Assert.Equal(9, tvma!.Value); + Assert.Equal(17, tvma!.Value); } [Fact] @@ -100,21 +100,21 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var ratings = localizationManager.GetParentalRatings().ToList(); - Assert.Equal(10, ratings.Count); + Assert.Equal(19, ratings.Count); var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal)); Assert.NotNull(fsk); - Assert.Equal(7, fsk!.Value); + Assert.Equal(12, fsk!.Value); } [Theory] - [InlineData("CA-R", "CA", 10)] - [InlineData("FSK-16", "DE", 8)] - [InlineData("FSK-18", "DE", 9)] - [InlineData("FSK-18", "US", 9)] - [InlineData("TV-MA", "US", 9)] - [InlineData("XXX", "asdf", 100)] - [InlineData("Germany: FSK-18", "DE", 9)] + [InlineData("CA-R", "CA", 18)] + [InlineData("FSK-16", "DE", 16)] + [InlineData("FSK-18", "DE", 18)] + [InlineData("FSK-18", "US", 18)] + [InlineData("TV-MA", "US", 17)] + [InlineData("XXX", "asdf", 1000)] + [InlineData("Germany: FSK-18", "DE", 18)] public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel) { var localizationManager = Setup(new ServerConfiguration() @@ -135,6 +135,9 @@ namespace Jellyfin.Server.Implementations.Tests.Localization UICulture = "de-DE" }); await localizationManager.LoadAll(); + Assert.Null(localizationManager.GetRatingLevel("NR")); + Assert.Null(localizationManager.GetRatingLevel("unrated")); + Assert.Null(localizationManager.GetRatingLevel("Not Rated")); Assert.Null(localizationManager.GetRatingLevel("n/a")); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index bc6a44741..d4b90dac0 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -1,7 +1,16 @@ using System; +using System.Globalization; using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using AutoFixture; +using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Plugins; +using Jellyfin.Extensions.Json; +using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -11,6 +20,21 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins { private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); + private string _tempPath = string.Empty; + + private string _pluginPath = string.Empty; + + private JsonSerializerOptions _options; + + public PluginManagerTests() + { + (_tempPath, _pluginPath) = GetTestPaths("plugin-" + Path.GetRandomFileName()); + + Directory.CreateDirectory(_pluginPath); + + _options = GetTestSerializerOptions(); + } + [Fact] public void SaveManifest_RoundTrip_Success() { @@ -20,12 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Version = "1.0" }; - var tempPath = Path.Combine(_testPathRoot, "manifest-" + Path.GetRandomFileName()); - Directory.CreateDirectory(tempPath); - - Assert.True(pluginManager.SaveManifest(manifest, tempPath)); + Assert.True(pluginManager.SaveManifest(manifest, _pluginPath)); - var res = pluginManager.LoadManifest(tempPath); + var res = pluginManager.LoadManifest(_pluginPath); Assert.Equal(manifest.Category, res.Manifest.Category); Assert.Equal(manifest.Changelog, res.Manifest.Changelog); @@ -40,6 +61,278 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Assert.Equal(manifest.Status, res.Manifest.Status); Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate); Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath); + Assert.Equal(manifest.Assemblies, res.Manifest.Assemblies); + } + + /// <summary> + /// Tests safe traversal within the plugin directory. + /// </summary> + /// <param name="dllFile">The safe path to evaluate.</param> + [Theory] + [InlineData("./some.dll")] + [InlineData("some.dll")] + [InlineData("sub/path/some.dll")] + public void Constructor_DiscoversSafePluginAssembly_Status_Active(string dllFile) + { + var manifest = new PluginManifest + { + Id = Guid.NewGuid(), + Name = "Safe Assembly", + Assemblies = new string[] { dllFile } + }; + + var filename = Path.GetFileName(dllFile)!; + var dllPath = Path.GetDirectoryName(Path.Combine(_pluginPath, dllFile))!; + + Directory.CreateDirectory(dllPath); + File.Create(Path.Combine(dllPath, filename)); + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + + File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options); + + var expectedFullPath = Path.Combine(_pluginPath, dllFile).Canonicalize(); + + Assert.NotNull(res); + Assert.NotEmpty(pluginManager.Plugins); + Assert.Equal(PluginStatus.Active, res!.Status); + Assert.Equal(expectedFullPath, pluginManager.Plugins[0].DllFiles[0]); + Assert.StartsWith(_pluginPath, expectedFullPath, StringComparison.InvariantCulture); + } + + /// <summary> + /// Tests unsafe attempts to traverse to higher directories. + /// </summary> + /// <remarks> + /// Attempts to load directories outside of the plugin should be + /// constrained. Path traversal, shell expansion, and double encoding + /// can be used to load unintended files. + /// See <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more. + /// </remarks> + /// <param name="unsafePath">The unsafe path to evaluate.</param> + [Theory] + [InlineData("/some.dll")] // Root path. + [InlineData("../some.dll")] // Simple traversal. + [InlineData("C:\\some.dll")] // Windows root path. + [InlineData("test.txt")] // Not a DLL + [InlineData(".././.././../some.dll")] // Traversal with current and parent + [InlineData("..\\.\\..\\.\\..\\some.dll")] // Windows traversal with current and parent + [InlineData("\\\\network\\resource.dll")] // UNC Path + [InlineData("https://jellyfin.org/some.dll")] // URL + [InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character. + public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath) + { + var manifest = new PluginManifest + { + Id = Guid.NewGuid(), + Name = "Unsafe Assembly", + Assemblies = new string[] { unsafePath } + }; + + // Only create very specific files. Otherwise the test will be exploiting path traversal. + var files = new string[] + { + "../other.dll", + "some.dll" + }; + + foreach (var file in files) + { + File.Create(Path.Combine(_pluginPath, file)); + } + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + + File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options); + + Assert.NotNull(res); + Assert.Empty(pluginManager.Plugins); + Assert.Equal(PluginStatus.Malfunctioned, res!.Status); + } + + [Fact] + public async Task PopulateManifest_ExistingMetafilePlugin_PopulatesMissingFields() + { + var packageInfo = GenerateTestPackage(); + + // Partial plugin without a name, but matching version and package ID + var partial = new PluginManifest + { + Id = packageInfo.Id, + AutoUpdate = false, // Turn off AutoUpdate + Status = PluginStatus.Restart, + Version = new Version(1, 0, 0).ToString(), + Assemblies = new[] { "Jellyfin.Test.dll" } + }; + + var expectedManifest = new PluginManifest + { + Id = partial.Id, + Name = packageInfo.Name, + AutoUpdate = partial.AutoUpdate, + Status = PluginStatus.Active, + Owner = packageInfo.Owner, + Assemblies = partial.Assemblies, + Category = packageInfo.Category, + Description = packageInfo.Description, + Overview = packageInfo.Overview, + TargetAbi = packageInfo.Versions[0].TargetAbi!, + Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), + Changelog = packageInfo.Versions[0].Changelog!, + Version = new Version(1, 0).ToString(), + ImagePath = string.Empty + }; + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equivalent(expectedManifest, result); + } + + [Fact] + public async Task PopulateManifest_NoMetafile_PreservesManifest() + { + var packageInfo = GenerateTestPackage(); + var expectedManifest = new PluginManifest + { + Id = packageInfo.Id, + Name = packageInfo.Name, + AutoUpdate = true, + Status = PluginStatus.Active, + Owner = packageInfo.Owner, + Assemblies = Array.Empty<string>(), + Category = packageInfo.Category, + Description = packageInfo.Description, + Overview = packageInfo.Overview, + TargetAbi = packageInfo.Versions[0].TargetAbi!, + Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), + Changelog = packageInfo.Versions[0].Changelog!, + Version = packageInfo.Versions[0].Version, + ImagePath = string.Empty + }; + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, null!, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equivalent(expectedManifest, result); + } + + [Fact] + public async Task PopulateManifest_ExistingMetafileMismatchedIds_Status_Malfunctioned() + { + var packageInfo = GenerateTestPackage(); + + // Partial plugin without a name, but matching version and package ID + var partial = new PluginManifest + { + Id = Guid.NewGuid(), + Version = new Version(1, 0, 0).ToString() + }; + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equal(packageInfo.Name, result.Name); + Assert.Equal(PluginStatus.Malfunctioned, result.Status); + } + + [Fact] + public async Task PopulateManifest_ExistingMetafileMismatchedVersions_Updates_Version() + { + var packageInfo = GenerateTestPackage(); + + var partial = new PluginManifest + { + Id = packageInfo.Id, + Version = new Version(2, 0, 0).ToString() + }; + + var metafilePath = Path.Combine(_pluginPath, "meta.json"); + File.WriteAllText(metafilePath, JsonSerializer.Serialize(partial, _options)); + + var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); + + await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); + + var resultBytes = File.ReadAllBytes(metafilePath); + var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); + + Assert.NotNull(result); + Assert.Equal(packageInfo.Name, result.Name); + Assert.Equal(PluginStatus.Active, result.Status); + Assert.Equal(packageInfo.Versions[0].Version, result.Version); + } + + private PackageInfo GenerateTestPackage() + { + var fixture = new Fixture(); + fixture.Customize<PackageInfo>(c => c.Without(x => x.Versions).Without(x => x.ImageUrl)); + fixture.Customize<VersionInfo>(c => c.Without(x => x.Version).Without(x => x.Timestamp)); + + var versionInfo = fixture.Create<VersionInfo>(); + versionInfo.Version = new Version(1, 0).ToString(); + versionInfo.Timestamp = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture); + + var packageInfo = fixture.Create<PackageInfo>(); + packageInfo.Versions = new[] { versionInfo }; + + return packageInfo; + } + + private JsonSerializerOptions GetTestSerializerOptions() + { + var options = new JsonSerializerOptions(JsonDefaults.Options) + { + WriteIndented = true + }; + + for (var i = 0; i < options.Converters.Count; i++) + { + // Remove the Guid converter for parity with plugin manager. + if (options.Converters[i] is JsonGuidConverter converter) + { + options.Converters.Remove(converter); + } + } + + return options; + } + + private (string TempPath, string PluginPath) GetTestPaths(string pluginFolderName) + { + var tempPath = Path.Combine(_testPathRoot, "plugin-manager" + Path.GetRandomFileName()); + var pluginPath = Path.Combine(tempPath, pluginFolderName); + + return (tempPath, pluginPath); } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs index 59d82678e..1dd49b2cf 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting Assert.Equal(-expected, cmp.Compare(y, x)); } - private class EpisodeBadData : TheoryData<BaseItem?, BaseItem?> + private sealed class EpisodeBadData : TheoryData<BaseItem?, BaseItem?> { public EpisodeBadData() { @@ -36,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Tests.Sorting } } - private class EpisodeTestData : TheoryData<BaseItem, BaseItem, int> + private sealed class EpisodeTestData : TheoryData<BaseItem, BaseItem, int> { public EpisodeTestData() { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml new file mode 100644 index 000000000..dd4aa8977 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml @@ -0,0 +1,6 @@ +<tv date="20221104"> + <programme channel="3297" start="20221104130000 -0400" stop="20221105235959 -0400"> + <category lang="en" /> + <category lang="en">sports</category> + </programme> +</tv> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json index fa8fbd8d2..57367ce88 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json @@ -681,4 +681,4 @@ } ] } -]
\ No newline at end of file +] diff --git a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs index 31f33c682..1bd51b246 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs @@ -50,7 +50,7 @@ namespace Jellyfin.Server.Implementations.Tests.TypedBaseItem private static bool IsProjectAssemblyName(string? name) { - if (name == null) + if (name is null) { return false; } diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 79c11a865..3737fee0a 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Models.StartupDtos; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dto; using Xunit; namespace Jellyfin.Server.Integration.Tests @@ -43,12 +44,39 @@ namespace Jellyfin.Server.Integration.Tests return auth!.AccessToken; } + public static async Task<UserDto> GetUserDtoAsync(HttpClient client) + { + using var response = await client.GetAsync("Users/Me").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var userDto = await JsonSerializer.DeserializeAsync<UserDto>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false); + Assert.NotNull(userDto); + return userDto; + } + + public static async Task<BaseItemDto> GetRootFolderDtoAsync(HttpClient client, Guid userId = default) + { + if (userId.Equals(default)) + { + var userDto = await GetUserDtoAsync(client).ConfigureAwait(false); + userId = userDto.Id; + } + + var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + JsonDefaults.Options).ConfigureAwait(false); + Assert.NotNull(rootDto); + return rootDto; + } + public static void AddAuthHeader(this HttpHeaders headers, string accessToken) { headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}"); } - private class AuthenticationResultDto + private sealed class AuthenticationResultDto { public string AccessToken { get; set; } = string.Empty; diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs index 1a720c2f6..3d4c27c95 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { return new ContentResult() { - Content = (@params != null && @params.Count > 0) + Content = (@params is not null && @params.Count > 0) ? string.Join("&", @params.Select(x => x.Key + "=" + x.Value)) : string.Empty, ContentType = "text/plain; charset=utf-8", @@ -42,7 +42,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { return new ContentResult() { - Content = (@params != null && @params.Count > 0) + Content = (@params is not null && @params.Count > 0) ? string.Join("&", @params.Select(x => x.Key + "=" + string.Join(',', x.Value))) : string.Empty, ContentType = "text/plain; charset=utf-8", diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs new file mode 100644 index 000000000..078002994 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static string? _accessToken; + + public ItemsControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetItems_NoApiKeyOrUserId_Success() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync("Items").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [InlineData("Users/{0}/Items")] + [InlineData("Users/{0}/Items/Resume")] + public async Task GetUserItems_NonExistentUserId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("Items?userId={0}")] + [InlineData("Users/{0}/Items")] + [InlineData("Users/{0}/Items/Resume")] + public async Task GetItems_UserId_Ok(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(items); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs new file mode 100644 index 000000000..8998683a7 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public LibraryControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Theory] + [InlineData("Items/{0}/File")] + [InlineData("Items/{0}/ThemeSongs")] + [InlineData("Items/{0}/ThemeVideos")] + [InlineData("Items/{0}/ThemeMedia")] + [InlineData("Items/{0}/Ancestors")] + [InlineData("Items/{0}/Download")] + [InlineData("Artists/{0}/Similar")] + [InlineData("Items/{0}/Similar")] + [InlineData("Albums/{0}/Similar")] + [InlineData("Shows/{0}/Similar")] + [InlineData("Movies/{0}/Similar")] + [InlineData("Trailers/{0}/Similar")] + public async Task Get_NonExistentItemId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("Items/{0}")] + [InlineData("Items?ids={0}")] + public async Task Delete_NonExistentItemId_Unauthorised(string format) + { + var client = _factory.CreateClient(); + + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [InlineData("Items/{0}")] + [InlineData("Items?ids={0}")] + public async Task Delete_NonExistentItemId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs new file mode 100644 index 000000000..17f3dc99f --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public MusicGenreControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task MusicGenres_FakeMusicGenre_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs new file mode 100644 index 000000000..868ecd53f --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public PlaystateControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task PostMarkPlayedItem_NonExistentUserId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task PostMarkPlayedItem_NonExistentItemId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs new file mode 100644 index 000000000..cb0a829e8 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public SessionControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetSessions_NonExistentUserId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 2b825a93a..2a3c53dbe 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -67,6 +67,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers } [Fact] + [Priority(-1)] + public async Task Me_Valid_Success() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + } + + [Fact] [Priority(0)] public async Task New_Valid_Success() { @@ -108,7 +118,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var createRequest = new CreateUserByName() { - Name = username + Name = username! }; using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); @@ -116,6 +126,19 @@ namespace Jellyfin.Server.Integration.Tests.Controllers } [Fact] + [Priority(0)] + public async Task Delete_DoesntExist_NotFound() + { + var client = _factory.CreateClient(); + + // access token can't be null here as the previous test populated it + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] [Priority(1)] public async Task UpdateUserPassword_Valid_Success() { diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs new file mode 100644 index 000000000..69f2ccf33 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -0,0 +1,129 @@ +using System; +using System.Globalization; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private static string? _accessToken; + + public UserLibraryControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetRootFolder_NonExistenUserId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetRootFolder_UserId_Valid() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + } + + [Theory] + [InlineData("Users/{0}/Items/{1}")] + [InlineData("Users/{0}/Items/{1}/Intros")] + [InlineData("Users/{0}/Items/{1}/LocalTrailers")] + [InlineData("Users/{0}/Items/{1}/SpecialFeatures")] + [InlineData("Users/{0}/Items/{1}/Lyrics")] + public async Task GetItem_NonExistenUserId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("Users/{0}/Items/{1}")] + [InlineData("Users/{0}/Items/{1}/Intros")] + [InlineData("Users/{0}/Items/{1}/LocalTrailers")] + [InlineData("Users/{0}/Items/{1}/SpecialFeatures")] + [InlineData("Users/{0}/Items/{1}/Lyrics")] + public async Task GetItem_NonExistentItemId_NotFound(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetItem_UserIdAndItemId_Valid() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(rootDto); + } + + [Fact] + public async Task GetIntros_UserIdAndItemId_Valid() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(rootDto); + } + + [Theory] + [InlineData("Users/{0}/Items/{1}/LocalTrailers")] + [InlineData("Users/{0}/Items/{1}/SpecialFeatures")] + public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format) + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + _jsonOptions).ConfigureAwait(false); + Assert.NotNull(rootDto); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs new file mode 100644 index 000000000..0f9a2e90a --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers; + +public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFactory> +{ + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public VideosControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task DeleteAlternateSources_NonExistentItemId_NotFound() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 659737392..a5296d8c9 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -1,25 +1,20 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.17.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> + <PackageReference Include="Microsoft.Extensions.Options" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Xunit.Priority" Version="1.1.6" /> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> + <PackageReference Include="Xunit.Priority" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="Moq" /> </ItemGroup> <ItemGroup> @@ -29,17 +24,6 @@ </None> </ItemGroup> - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" /> </ItemGroup> diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 48c49bf84..55bc43455 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -1,14 +1,19 @@ using System; using System.Collections.Concurrent; +using System.Globalization; using System.IO; using System.Threading; using Emby.Server.Implementations; +using Jellyfin.Server.Extensions; +using Jellyfin.Server.Helpers; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; @@ -28,8 +33,10 @@ namespace Jellyfin.Server.Integration.Tests static JellyfinApplicationFactory() { // Perform static initialization that only needs to happen once per test-run - Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); - Program.PerformStaticInitialization(); + Log.Logger = new LoggerConfiguration() + .WriteTo.Console(formatProvider: CultureInfo.InvariantCulture) + .CreateLogger(); + StartupHelpers.PerformStaticInitialization(); } /// <inheritdoc/> @@ -59,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests // Create the logging config file // TODO: We shouldn't need to do this since we are only logging to console - Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); + StartupHelpers.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); // Create a copy of the application configuration to use for startup var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); @@ -75,11 +82,17 @@ namespace Jellyfin.Server.Integration.Tests commandLineOpts, startupConfig); _disposableComponents.Add(appHost); - var serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); - // Configure the web host builder - Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths); + builder.ConfigureServices(services => appHost.Init(services)) + .ConfigureWebHostBuilder(appHost, startupConfig, appPaths, NullLogger.Instance) + .ConfigureAppConfiguration((context, builder) => + { + builder + .SetBasePath(appPaths.ConfigurationDirectoryPath) + .AddInMemoryCollection(ConfigurationOptions.DefaultConfiguration) + .AddEnvironmentVariables("JELLYFIN_") + .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); + }); } /// <inheritdoc/> diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index b2653b28b..5fea805ae 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -1,36 +1,19 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.17.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.9" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> + <PackageReference Include="Microsoft.Extensions.Options" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> + <PackageReference Include="Moq" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs index d15c9d6f5..797fc8f64 100644 --- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs +++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Jellyfin.Server.Middleware; +using Jellyfin.Api.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index f03448eed..9fe0744de 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -1,11 +1,5 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <IsPackable>false</IsPackable> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <None Include="Test Data\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -13,25 +7,14 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> - <PackageReference Include="Moq" Version="4.18.2" /> - <PackageReference Include="xunit" Version="2.4.2" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="3.1.2" /> - </ItemGroup> - - <!-- Code Analyzers --> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + <PackageReference Include="coverlet.collector" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index 4f4ae5afb..f63bc0e1b 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -79,18 +80,18 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal("1276153", item.ProviderIds[MetadataProvider.Tmdb.ToString()]); // Credits - var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray(); + var writers = result.People.Where(x => x.Type == PersonKind.Writer).ToArray(); Assert.Equal(2, writers.Length); Assert.Contains("Bryan Fuller", writers.Select(x => x.Name)); Assert.Contains("Michael Green", writers.Select(x => x.Name)); // Direcotrs - var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray(); + var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray(); Assert.Single(directors); Assert.Contains("David Slade", directors.Select(x => x.Name)); // Actors - var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray(); + var actors = result.People.Where(x => x.Type == PersonKind.Actor).ToArray(); Assert.Equal(11, actors.Length); // Only test one actor var shadow = actors.FirstOrDefault(x => x.Role.Equals("Shadow Moon", StringComparison.Ordinal)); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 988abce81..f56f58c6f 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -117,18 +118,18 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(20, result.People.Count); - var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray(); + var writers = result.People.Where(x => x.Type == PersonKind.Writer).ToArray(); Assert.Equal(3, writers.Length); var writerNames = writers.Select(x => x.Name); Assert.Contains("Jerry Siegel", writerNames); Assert.Contains("Joe Shuster", writerNames); Assert.Contains("Test", writerNames); - var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray(); + var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray(); Assert.Single(directors); Assert.Equal("Zack Snyder", directors[0].Name); - var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray(); + var actors = result.People.Where(x => x.Type == PersonKind.Actor).ToArray(); Assert.Equal(15, actors.Length); // Only test one actor @@ -138,7 +139,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(5, aquaman!.SortOrder); Assert.Equal("https://m.media-amazon.com/images/M/MV5BMTI5MTU5NjM1MV5BMl5BanBnXkFtZTcwODc4MDk0Mw@@._V1_SX1024_SY1024_.jpg", aquaman!.ImageUrl); - var lyricist = result.People.FirstOrDefault(x => x.Type == PersonType.Lyricist); + var lyricist = result.People.FirstOrDefault(x => x.Type == PersonKind.Lyricist); Assert.NotNull(lyricist); Assert.Equal("Test Lyricist", lyricist!.Name); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs index 31110dbd7..e69ca996c 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -60,7 +61,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(10, result.People.Count); - Assert.True(result.People.All(x => x.Type == PersonType.Actor)); + Assert.True(result.People.All(x => x.Type == PersonKind.Actor)); // Only test one actor var nini = result.People.FirstOrDefault(x => x.Role.Equals("Nini", StringComparison.Ordinal)); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs index bdedae205..f680d2dcc 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -67,7 +68,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(6, result.People.Count); - Assert.True(result.People.All(x => x.Type == PersonType.Actor)); + Assert.True(result.People.All(x => x.Type == PersonKind.Actor)); // Only test one actor var sweeney = result.People.FirstOrDefault(x => x.Role.Equals("Mad Sweeney", StringComparison.Ordinal)); @@ -89,7 +90,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers }; _parser.Fetch(result, path, CancellationToken.None); - var item = (Series)result.Item; + var item = result.Item; Assert.Equal(id, item.ProviderIds[provider]); } |
