aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDominik <git@secnd.me>2023-06-15 19:38:42 +0200
committerGitHub <noreply@github.com>2023-06-15 19:38:42 +0200
commit17f1e8d19b1fd693893d66d2275ed8ae2476344e (patch)
tree7f48be975faa92042769870957587b3c7864f631 /tests
parente8ae7e5c38e28f13fa8de295e26c930cb46d9b79 (diff)
parent6771b5cabe96b4b3cbd1cd0c998d564f3dd17ed4 (diff)
Merge branch 'master' into segment-deletion
Diffstat (limited to 'tests')
-rw-r--r--tests/Directory.Build.props23
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs32
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs (renamed from tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs)18
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs8
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs59
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs53
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs35
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs80
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj37
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs2
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj27
-rw-r--r--tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs32
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs25
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj27
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj27
-rw-r--r--tests/Jellyfin.Dlna.Tests/Server/DescriptionXmlBuilderTests.cs47
-rw-r--r--tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs7
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj27
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs37
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs2
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs6
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj24
-rw-r--r--tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj26
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs6
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs24
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs25
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj33
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs136
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs34
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_interlaced.json81
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order.json133
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_progressive_no_field_order2.json72
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json (renamed from tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json)0
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs139
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj29
-rw-r--r--tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj27
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs5
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs63
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs13
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs4
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj29
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs1
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj27
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs6
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs19
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs614
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs13
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/HttpServer/WebSocketConnectionTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj34
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs76
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs12
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs29
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MediaStreamSelectorTests.cs64
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs43
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs19
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs303
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml6
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs30
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs64
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs63
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs26
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs61
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs25
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs129
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj38
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs27
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj37
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj27
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs7
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs9
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs3
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs5
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]);
}