Merge remote-tracking branch 'upstream/master' into baseitemkind-fixes

This commit is contained in:
crobibero
2021-06-20 07:09:24 -06:00
1066 changed files with 19285 additions and 12379 deletions

View File

@@ -128,6 +128,8 @@ namespace Jellyfin.Api.Tests.Auth
{
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
authorizationInfo.User = _fixture.Create<User>();
authorizationInfo.User.AddDefaultPermissions();
authorizationInfo.User.AddDefaultPreferences();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
authorizationInfo.IsApiKey = false;

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
@@ -41,7 +42,7 @@ namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
{
_networkManagerMock
.Setup(n => n.IsInLocalNetwork(It.IsAny<string>()))
.Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>()))
.Returns(isInLocalNetwork);
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Controllers;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers
{
public class DynamicHlsControllerTests
{
[Theory]
[MemberData(nameof(GetSegmentLengths_Success_TestData))]
public void GetSegmentLengths_Success(long runtimeTicks, int segmentlength, double[] expected)
{
var res = DynamicHlsController.GetSegmentLengthsInternal(runtimeTicks, segmentlength);
Assert.Equal(expected.Length, res.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], res[i]);
}
}
public static IEnumerable<object[]> GetSegmentLengths_Success_TestData()
{
yield return new object[] { 0, 6, Array.Empty<double>() };
yield return new object[]
{
TimeSpan.FromSeconds(3).Ticks,
6,
new double[] { 3 }
};
yield return new object[]
{
TimeSpan.FromSeconds(6).Ticks,
6,
new double[] { 6 }
};
yield return new object[]
{
TimeSpan.FromSeconds(3.3333333).Ticks,
6,
new double[] { 3.3333333 }
};
yield return new object[]
{
TimeSpan.FromSeconds(9.3333333).Ticks,
6,
new double[] { 6, 3.3333333 }
};
}
}
}

View File

@@ -10,35 +10,33 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.15.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.3" />
<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="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
<ProjectReference Include="../../Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -1,17 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Jellyfin.Api.Tests.ModelBinders
{
public enum TestType
{
#pragma warning disable SA1602 // Enumeration items should be documented
How,
Much,
Is,
The,
Fish
#pragma warning restore SA1602 // Enumeration items should be documented
}
}

View File

@@ -26,8 +26,11 @@ namespace Jellyfin.Api.Tests
{
var user = new User(
"jellyfin",
typeof(DefaultAuthenticationProvider).FullName,
typeof(DefaultPasswordResetProvider).FullName);
typeof(DefaultAuthenticationProvider).FullName!,
typeof(DefaultPasswordResetProvider).FullName!);
user.AddDefaultPermissions();
user.AddDefaultPreferences();
// Set administrator flag.
user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));

View File

@@ -0,0 +1,33 @@
using System;
using System.Text;
using MediaBrowser.Common;
using Xunit;
namespace Jellyfin.Common.Tests
{
public static class Crc32Tests
{
[Fact]
public static void Compute_Empty_Zero()
{
Assert.Equal<uint>(0, Crc32.Compute(Array.Empty<byte>()));
}
[Theory]
[InlineData(0x414fa339, "The quick brown fox jumps over the lazy dog")]
public static void Compute_Valid_Success(uint expected, string data)
{
Assert.Equal(expected, Crc32.Compute(Encoding.UTF8.GetBytes(data)));
}
[Theory]
[InlineData(0x414fa339, "54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67")]
[InlineData(0x190a55ad, "0000000000000000000000000000000000000000000000000000000000000000")]
[InlineData(0xff6cab0b, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")]
[InlineData(0x91267e8a, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F")]
public static void Compute_ValidHex_Success(uint expected, string data)
{
Assert.Equal(expected, Crc32.Compute(Convert.FromHexString(data)));
}
}
}

View File

@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Cryptography;
using Xunit;
namespace Jellyfin.Common.Tests.Cryptography
{
public static class PasswordHashTests
{
[Fact]
public static void Ctor_Null_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new PasswordHash(null!, Array.Empty<byte>()));
}
[Fact]
public static void Ctor_Empty_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new PasswordHash(string.Empty, Array.Empty<byte>()));
}
public static IEnumerable<object[]> Parse_Valid_TestData()
{
// Id
yield return new object[]
{
"$PBKDF2",
new PasswordHash("PBKDF2", Array.Empty<byte>())
};
// Id + parameter
yield return new object[]
{
"$PBKDF2$iterations=1000",
new PasswordHash(
"PBKDF2",
Array.Empty<byte>(),
Array.Empty<byte>(),
new Dictionary<string, string>()
{
{ "iterations", "1000" },
})
};
// Id + parameters
yield return new object[]
{
"$PBKDF2$iterations=1000,m=120",
new PasswordHash(
"PBKDF2",
Array.Empty<byte>(),
Array.Empty<byte>(),
new Dictionary<string, string>()
{
{ "iterations", "1000" },
{ "m", "120" }
})
};
// Id + hash
yield return new object[]
{
"$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Array.Empty<byte>(),
new Dictionary<string, string>())
};
// Id + salt + hash
yield return new object[]
{
"$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Convert.FromHexString("69F420"),
new Dictionary<string, string>())
};
// Id + parameter + hash
yield return new object[]
{
"$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Array.Empty<byte>(),
new Dictionary<string, string>()
{
{ "iterations", "1000" }
})
};
// Id + parameters + hash
yield return new object[]
{
"$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Array.Empty<byte>(),
new Dictionary<string, string>()
{
{ "iterations", "1000" },
{ "m", "120" }
})
};
// Id + parameters + salt + hash
yield return new object[]
{
"$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Convert.FromHexString("69F420"),
new Dictionary<string, string>()
{
{ "iterations", "1000" },
{ "m", "120" }
})
};
}
[Theory]
[MemberData(nameof(Parse_Valid_TestData))]
public static void Parse_Valid_Success(string passwordHashString, PasswordHash expected)
{
var passwordHash = PasswordHash.Parse(passwordHashString);
Assert.Equal(expected.Id, passwordHash.Id);
Assert.Equal(expected.Parameters, passwordHash.Parameters);
Assert.Equal(expected.Salt.ToArray(), passwordHash.Salt.ToArray());
Assert.Equal(expected.Hash.ToArray(), passwordHash.Hash.ToArray());
Assert.Equal(expected.ToString(), passwordHash.ToString());
}
[Theory]
[InlineData("$PBKDF2")]
[InlineData("$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
[InlineData("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
[InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
[InlineData("$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
[InlineData("$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
[InlineData("$PBKDF2$iterations=1000,m=120")]
public static void ToString_Roundtrip_Success(string passwordHash)
{
Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString());
}
[Fact]
public static void Parse_Null_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => PasswordHash.Parse(null));
}
[Fact]
public static void Parse_Empty_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => PasswordHash.Parse(string.Empty));
}
[Theory]
[InlineData("$")] // No id
[InlineData("$$")] // Empty segments
[InlineData("PBKDF2$")] // Doesn't start with $
[InlineData("$PBKDF2$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty segment
[InlineData("$PBKDF2$iterations=1000$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty salt segment
[InlineData("$PBKDF2$iterations=1000$69F420$")] // Empty hash segment
[InlineData("$PBKDF2$=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
[InlineData("$PBKDF2$=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
[InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
[InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Ends on $
[InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Extra segment
[InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$anotherone")] // Extra segment
[InlineData("$PBKDF2$iterations=$invalidstalt$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid salt
[InlineData("$PBKDF2$iterations=$69F420$invalid hash")] // Invalid hash
[InlineData("$PBKDF2$69F420$")] // Empty hash
public static void Parse_InvalidFormat_ThrowsFormatException(string passwordHash)
{
Assert.Throws<FormatException>(() => PasswordHash.Parse(passwordHash));
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Extensions;
using Xunit;
namespace Jellyfin.Common.Tests.Extensions
{
public static class CopyToExtensionsTests
{
public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData()
{
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } };
yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } };
}
[Theory]
[MemberData(nameof(CopyTo_Valid_Correct_TestData))]
public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected)
{
source.CopyTo(destination, index);
Assert.Equal(expected, destination);
}
public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
{
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 };
yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 };
yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 };
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 };
yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 };
}
[Theory]
[MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))]
public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index)
{
Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index));
}
}
}

View File

@@ -1,43 +0,0 @@
using System;
using MediaBrowser.Common.Extensions;
using Xunit;
namespace Jellyfin.Common.Tests.Extensions
{
public class StringExtensionsTests
{
[Theory]
[InlineData("", 'q', "")]
[InlineData("Banana split", ' ', "Banana")]
[InlineData("Banana split", 'q', "Banana split")]
public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
{
var result = str.AsSpan().LeftPart(needle).ToString();
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("", "", "")]
[InlineData("", "q", "")]
[InlineData("Banana split", "", "")]
[InlineData("Banana split", " ", "Banana")]
[InlineData("Banana split test", " split", "Banana")]
public void LeftPart_ValidArgsWithoutStringComparison_Correct(string str, string needle, string expectedResult)
{
var result = str.AsSpan().LeftPart(needle).ToString();
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("", "", StringComparison.Ordinal, "")]
[InlineData("Banana split", " ", StringComparison.Ordinal, "Banana")]
[InlineData("Banana split test", " split", StringComparison.Ordinal, "Banana")]
[InlineData("Banana split test", " Split", StringComparison.Ordinal, "Banana split test")]
[InlineData("Banana split test", " Splït", StringComparison.InvariantCultureIgnoreCase, "Banana split test")]
public void LeftPart_ValidArgs_Correct(string str, string needle, StringComparison stringComparison, string expectedResult)
{
var result = str.AsSpan().LeftPart(needle, stringComparison).ToString();
Assert.Equal(expectedResult, result);
}
}
}

View File

@@ -10,18 +10,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -32,8 +34,4 @@
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -1,34 +1,45 @@
using System.Text.Json;
using System.Globalization;
using System.Text.Json;
using FsCheck;
using FsCheck.Xunit;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
public static class JsonBoolNumberTests
public class JsonBoolNumberTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
{
Converters =
{
new JsonBoolNumberConverter()
}
};
[Theory]
[InlineData("1", true)]
[InlineData("0", false)]
[InlineData("2", true)]
[InlineData("true", true)]
[InlineData("false", false)]
public static void Deserialize_Number_Valid_Success(string input, bool? output)
public void Deserialize_Number_Valid_Success(string input, bool? output)
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonBoolNumberConverter());
var value = JsonSerializer.Deserialize<bool>(input, options);
var value = JsonSerializer.Deserialize<bool>(input, _jsonOptions);
Assert.Equal(value, output);
}
[Theory]
[InlineData(true, "true")]
[InlineData(false, "false")]
public static void Serialize_Bool_Success(bool input, string output)
public void Serialize_Bool_Success(bool input, string output)
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonBoolNumberConverter());
var value = JsonSerializer.Serialize(input, options);
var value = JsonSerializer.Serialize(input, _jsonOptions);
Assert.Equal(value, output);
}
[Property]
public Property Deserialize_NonZeroInt_True(NonZeroInt input)
=> JsonSerializer.Deserialize<bool>(input.ToString(), _jsonOptions).ToProperty();
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Common.Tests.Models;
using MediaBrowser.Model.Session;
@@ -8,6 +9,27 @@ namespace Jellyfin.Common.Tests.Json
{
public static class JsonCommaDelimitedArrayTests
{
[Fact]
public static void Deserialize_String_Null_Success()
{
var options = new JsonSerializerOptions();
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", options);
Assert.Null(value?.Value);
}
[Fact]
public static void Deserialize_Empty_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
{
Value = Array.Empty<string>()
};
var options = new JsonSerializerOptions();
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_String_Valid_Success()
{
@@ -48,6 +70,34 @@ namespace Jellyfin.Common.Tests.Json
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_EmptyEntry_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Invalid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
};
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options);
Assert.Equal(desiredValue.Value, value?.Value);
}
[Fact]
public static void Deserialize_GenericCommandType_Space_Valid_Success()
{

View File

@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;

View File

@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;

View File

@@ -38,6 +38,15 @@ namespace Jellyfin.Common.Tests.Json
Assert.Null(result);
}
[Theory]
[InlineData("\"8\"", 8)]
[InlineData("8", 8)]
public void Deserialize_NullableInt_Success(string input, int? expected)
{
var result = JsonSerializer.Deserialize<int?>(input, _options);
Assert.Equal(result, expected);
}
[Theory]
[InlineData("\"N/A\"")]
[InlineData("null")]
@@ -48,21 +57,11 @@ namespace Jellyfin.Common.Tests.Json
}
[Theory]
[InlineData("\"8\"", 8)]
[InlineData("8", 8)]
public void Deserialize_Int_Success(string input, int expected)
[InlineData("\"Jellyfin\"", "Jellyfin")]
public void Deserialize_Normal_String_Success(string input, string expected)
{
var result = JsonSerializer.Deserialize<int>(input, _options);
Assert.Equal(result, expected);
}
[Fact]
public void Deserialize_Normal_String_Success()
{
const string Input = "\"Jellyfin\"";
const string Expected = "Jellyfin";
var result = JsonSerializer.Deserialize<string>(Input, _options);
Assert.Equal(Expected, result);
var result = JsonSerializer.Deserialize<string?>(input, _options);
Assert.Equal(expected, result);
}
[Fact]

View File

@@ -0,0 +1,38 @@
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
public class JsonStringConverterTests
{
private readonly JsonSerializerOptions _jsonSerializerOptions = new ()
{
Converters =
{
new JsonStringConverter()
}
};
[Theory]
[InlineData("\"test\"", "test")]
[InlineData("123", "123")]
[InlineData("123.45", "123.45")]
[InlineData("true", "true")]
[InlineData("false", "false")]
public void Deserialize_String_Valid_Success(string input, string output)
{
var deserialized = JsonSerializer.Deserialize<string>(input, _jsonSerializerOptions);
Assert.Equal(deserialized, output);
}
[Fact]
public void Deserialize_Int32asInt32_Valid_Success()
{
const string? input = "123";
const int output = 123;
var deserialized = JsonSerializer.Deserialize<int>(input, _jsonSerializerOptions);
Assert.Equal(deserialized, output);
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
public class JsonVersionConverterTests
{
private readonly JsonSerializerOptions _options;
public JsonVersionConverterTests()
{
_options = new JsonSerializerOptions();
_options.Converters.Add(new JsonVersionConverter());
}
[Fact]
public void Deserialize_Version_Success()
{
var input = "\"1.025.222\"";
var output = new Version(1, 25, 222);
var deserializedInput = JsonSerializer.Deserialize<Version>(input, _options);
Assert.Equal(output, deserializedInput);
}
[Fact]
public void Serialize_Version_Success()
{
var input = new Version(1, 09, 59);
var output = "\"1.9.59\"";
var serializedInput = JsonSerializer.Serialize(input, _options);
Assert.Equal(output, serializedInput);
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using MediaBrowser.Common;
using MediaBrowser.Common.Cryptography;
using Xunit;
namespace Jellyfin.Common.Tests
{
public class PasswordHashTests
{
[Theory]
[InlineData(
"$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
"PBKDF2",
"",
"62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
public void ParseTest(string passwordHash, string id, string salt, string hash)
{
var pass = PasswordHash.Parse(passwordHash);
Assert.Equal(id, pass.Id);
Assert.Equal(salt, Convert.ToHexString(pass.Salt));
Assert.Equal(hash, Convert.ToHexString(pass.Hash));
}
[Theory]
[InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")]
public void ToStringTest(string passwordHash)
{
Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString());
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using MediaBrowser.Common.Providers;
using Xunit;
namespace Jellyfin.Common.Tests.Providers
{
public class ProviderIdParserTests
{
[Theory]
[InlineData("tt1234567", "tt1234567")]
[InlineData("tt12345678", "tt12345678")]
[InlineData("https://www.imdb.com/title/tt1234567", "tt1234567")]
[InlineData("https://www.imdb.com/title/tt12345678", "tt12345678")]
[InlineData(@"multiline\nhttps://www.imdb.com/title/tt1234567", "tt1234567")]
[InlineData(@"multiline\nhttps://www.imdb.com/title/tt12345678", "tt12345678")]
[InlineData("tt1234567tt7654321", "tt1234567")]
[InlineData("tt12345678tt7654321", "tt12345678")]
[InlineData("tt123456789", "tt12345678")]
public void FindImdbId_Valid_Success(string text, string expected)
{
Assert.True(ProviderIdParsers.TryFindImdbId(text, out ReadOnlySpan<char> parsedId));
Assert.Equal(expected, parsedId.ToString());
}
[Theory]
[InlineData("tt123456")]
[InlineData("https://www.imdb.com/title/tt123456")]
[InlineData("Jellyfin")]
public void FindImdbId_Invalid_Success(string text)
{
Assert.False(ProviderIdParsers.TryFindImdbId(text, out _));
}
[Theory]
[InlineData("https://www.themoviedb.org/movie/30287-fallo", "30287")]
[InlineData("themoviedb.org/movie/30287", "30287")]
public void FindTmdbMovieId_Valid_Success(string text, string expected)
{
Assert.True(ProviderIdParsers.TryFindTmdbMovieId(text, out ReadOnlySpan<char> parsedId));
Assert.Equal(expected, parsedId.ToString());
}
[Theory]
[InlineData("https://www.themoviedb.org/movie/fallo-30287")]
[InlineData("https://www.themoviedb.org/tv/1668-friends")]
public void FindTmdbMovieId_Invalid_Success(string text)
{
Assert.False(ProviderIdParsers.TryFindTmdbMovieId(text, out _));
}
[Theory]
[InlineData("https://www.themoviedb.org/tv/1668-friends", "1668")]
[InlineData("themoviedb.org/tv/1668", "1668")]
public void FindTmdbSeriesId_Valid_Success(string text, string expected)
{
Assert.True(ProviderIdParsers.TryFindTmdbSeriesId(text, out ReadOnlySpan<char> parsedId));
Assert.Equal(expected, parsedId.ToString());
}
[Theory]
[InlineData("https://www.themoviedb.org/tv/friends-1668")]
[InlineData("https://www.themoviedb.org/movie/30287-fallo")]
public void FindTmdbSeriesId_Invalid_Success(string text)
{
Assert.False(ProviderIdParsers.TryFindTmdbSeriesId(text, out _));
}
[Theory]
[InlineData("https://www.thetvdb.com/?tab=series&id=121361", "121361")]
[InlineData("thetvdb.com/?tab=series&id=121361", "121361")]
public void FindTvdbId_Valid_Success(string text, string expected)
{
Assert.True(ProviderIdParsers.TryFindTvdbId(text, out ReadOnlySpan<char> parsedId));
Assert.Equal(expected, parsedId.ToString());
}
[Theory]
[InlineData("thetvdb.com/?tab=series&id=Jellyfin121361")]
[InlineData("https://www.themoviedb.org/tv/1668-friends")]
public void FindTvdbId_Invalid_Success(string text)
{
Assert.False(ProviderIdParsers.TryFindTvdbId(text, out _));
}
}
}

View File

@@ -0,0 +1,200 @@
using System.Linq;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Controller.Tests
{
public class DirectoryServiceTests
{
private const string LowerCasePath = "/music/someartist";
private const string UpperCasePath = "/music/SOMEARTIST";
private static readonly FileSystemMetadata[] _lowerCaseFileSystemMetadata =
{
new ()
{
FullName = LowerCasePath + "/Artwork",
IsDirectory = true
},
new ()
{
FullName = LowerCasePath + "/Some Other Folder",
IsDirectory = true
},
new ()
{
FullName = LowerCasePath + "/Song 2.mp3",
IsDirectory = false
},
new ()
{
FullName = LowerCasePath + "/Song 3.mp3",
IsDirectory = false
}
};
private static readonly FileSystemMetadata[] _upperCaseFileSystemMetadata =
{
new ()
{
FullName = UpperCasePath + "/Lyrics",
IsDirectory = true
},
new ()
{
FullName = UpperCasePath + "/Song 1.mp3",
IsDirectory = false
}
};
[Fact]
public void GetFileSystemEntries_GivenPathsWithDifferentCasing_CachesAll()
{
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
var upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath);
var lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata, upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata, lowerCaseResult);
}
[Fact]
public void GetFiles_GivenPathsWithDifferentCasing_ReturnsCorrectFiles()
{
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
var upperCaseResult = directoryService.GetFiles(UpperCasePath);
var lowerCaseResult = directoryService.GetFiles(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult);
}
[Fact]
public void GetFile_GivenFilePathsWithDifferentCasing_ReturnsCorrectFile()
{
const string lowerCasePath = "/music/someartist/song 1.mp3";
var lowerCaseFileSystemMetadata = new FileSystemMetadata
{
FullName = lowerCasePath,
Exists = true
};
const string upperCasePath = "/music/SOMEARTIST/SONG 1.mp3";
var upperCaseFileSystemMetadata = new FileSystemMetadata
{
FullName = upperCasePath,
Exists = false
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
var lowerCaseResult = directoryService.GetFile(lowerCasePath);
var upperCaseResult = directoryService.GetFile(upperCasePath);
Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseResult);
Assert.Null(upperCaseResult);
}
[Fact]
public void GetFile_GivenCachedPath_ReturnsCachedFile()
{
const string path = "/music/someartist/song 1.mp3";
var cachedFileSystemMetadata = new FileSystemMetadata
{
FullName = path,
Exists = true
};
var newFileSystemMetadata = new FileSystemMetadata
{
FullName = "/music/SOMEARTIST/song 1.mp3",
Exists = true
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
var result = directoryService.GetFile(path);
fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
var secondResult = directoryService.GetFile(path);
Assert.Equal(cachedFileSystemMetadata, result);
Assert.Equal(cachedFileSystemMetadata, secondResult);
}
[Fact]
public void GetFilePaths_GivenCachedFilePathWithoutClear_ReturnsOnlyCachedPaths()
{
const string path = "/music/someartist";
var cachedPaths = new[]
{
"/music/someartist/song 1.mp3",
"/music/someartist/song 2.mp3",
"/music/someartist/song 3.mp3",
"/music/someartist/song 4.mp3",
};
var newPaths = new[]
{
"/music/someartist/song 5.mp3",
"/music/someartist/song 6.mp3",
"/music/someartist/song 7.mp3",
"/music/someartist/song 8.mp3",
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path);
Assert.Equal(cachedPaths, result);
Assert.Equal(cachedPaths, secondResult);
}
[Fact]
public void GetFilePaths_GivenCachedFilePathWithClear_ReturnsNewPaths()
{
const string path = "/music/someartist";
var cachedPaths = new[]
{
"/music/someartist/song 1.mp3",
"/music/someartist/song 2.mp3",
"/music/someartist/song 3.mp3",
"/music/someartist/song 4.mp3",
};
var newPaths = new[]
{
"/music/someartist/song 5.mp3",
"/music/someartist/song 6.mp3",
"/music/someartist/song 7.mp3",
"/music/someartist/song 8.mp3",
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path, true);
Assert.Equal(cachedPaths, result);
Assert.Equal(newPaths, secondResult);
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using MediaBrowser.Controller.Extensions;
using Xunit;
namespace Jellyfin.Controller.Extensions.Tests
{
public class StringExtensionsTests
{
[Theory]
[InlineData("", '_', 0)]
[InlineData("___", '_', 3)]
[InlineData("test\x00", '\x00', 1)]
[InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)]
public void ReadOnlySpan_Count_Success(string str, char needle, int count)
{
Assert.Equal(count, str.AsSpan().Count(needle));
}
}
}

View File

@@ -10,18 +10,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -31,8 +33,4 @@
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,131 @@
using Emby.Dlna;
using Emby.Dlna.PlayTo;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Jellyfin.Dlna.Tests
{
public class DlnaManagerTests
{
private DlnaManager GetManager()
{
var xmlSerializer = new Mock<IXmlSerializer>();
var fileSystem = new Mock<IFileSystem>();
var appPaths = new Mock<IApplicationPaths>();
var loggerFactory = new Mock<ILoggerFactory>();
var appHost = new Mock<IServerApplicationHost>();
return new DlnaManager(xmlSerializer.Object, fileSystem.Object, appPaths.Object, loggerFactory.Object, appHost.Object);
}
[Fact]
public void IsMatch_GivenMatchingName_ReturnsTrue()
{
var device = new DeviceInfo()
{
Name = "My Device",
Manufacturer = "LG Electronics",
ManufacturerUrl = "http://www.lge.com",
ModelDescription = "LG WebOSTV DMRplus",
ModelName = "LG TV",
ModelNumber = "1.0",
};
var profile = new DeviceProfile()
{
Name = "Test Profile",
FriendlyName = "My Device",
Manufacturer = "LG Electronics",
ManufacturerUrl = "http://www.lge.com",
ModelDescription = "LG WebOSTV DMRplus",
ModelName = "LG TV",
ModelNumber = "1.0",
Identification = new ()
{
FriendlyName = "My Device",
Manufacturer = "LG Electronics",
ManufacturerUrl = "http://www.lge.com",
ModelDescription = "LG WebOSTV DMRplus",
ModelName = "LG TV",
ModelNumber = "1.0",
}
};
var profile2 = new DeviceProfile()
{
Name = "Test Profile",
FriendlyName = "My Device",
Identification = new DeviceIdentification()
{
FriendlyName = "My Device",
}
};
var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile2.Identification);
var deviceMatch2 = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification);
Assert.True(deviceMatch);
Assert.True(deviceMatch2);
}
[Fact]
public void IsMatch_GivenNamesAndManufacturersDoNotMatch_ReturnsFalse()
{
var device = new DeviceInfo()
{
Name = "My Device",
Manufacturer = "JVC"
};
var profile = new DeviceProfile()
{
Name = "Test Profile",
FriendlyName = "My Device",
Manufacturer = "LG Electronics",
ManufacturerUrl = "http://www.lge.com",
ModelDescription = "LG WebOSTV DMRplus",
ModelName = "LG TV",
ModelNumber = "1.0",
Identification = new ()
{
FriendlyName = "My Device",
Manufacturer = "LG Electronics",
ManufacturerUrl = "http://www.lge.com",
ModelDescription = "LG WebOSTV DMRplus",
ModelName = "LG TV",
ModelNumber = "1.0",
}
};
var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification);
Assert.False(deviceMatch);
}
[Fact]
public void IsMatch_GivenNamesAndRegExMatch_ReturnsTrue()
{
var device = new DeviceInfo()
{
Name = "My Device"
};
var profile = new DeviceProfile()
{
Name = "Test Profile",
FriendlyName = "My .*",
Identification = new ()
};
var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification);
Assert.True(deviceMatch);
}
}
}

View File

@@ -5,18 +5,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -26,8 +28,4 @@
<ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -16,7 +16,7 @@ namespace Jellyfin.MediaEncoding.Tests
var path = Path.Join("Test Data", fileName);
using (var stream = File.OpenRead(path))
{
await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.GetOptions()).ConfigureAwait(false);
await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
}
}
}

View File

@@ -10,6 +10,8 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -19,15 +21,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -37,8 +38,4 @@
<ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,75 @@
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using MediaBrowser.Common.Json;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests.Probing
{
public class ProbeResultNormalizerTests
{
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null);
[Fact]
public void GetMediaInfo_MetaData_Success()
{
var bytes = File.ReadAllBytes("Test Data/Probing/video_metadata.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.Single(res.MediaStreams);
Assert.NotNull(res.VideoStream);
Assert.Equal("4:3", res.VideoStream.AspectRatio);
Assert.Equal(25f, res.VideoStream.AverageFrameRate);
Assert.Equal(8, res.VideoStream.BitDepth);
Assert.Equal(69432, res.VideoStream.BitRate);
Assert.Equal("h264", res.VideoStream.Codec);
Assert.Equal("1/50", res.VideoStream.CodecTimeBase);
Assert.Equal(240, res.VideoStream.Height);
Assert.Equal(320, res.VideoStream.Width);
Assert.Equal(0, res.VideoStream.Index);
Assert.False(res.VideoStream.IsAnamorphic);
Assert.True(res.VideoStream.IsAVC);
Assert.True(res.VideoStream.IsDefault);
Assert.False(res.VideoStream.IsExternal);
Assert.False(res.VideoStream.IsForced);
Assert.False(res.VideoStream.IsInterlaced);
Assert.False(res.VideoStream.IsTextSubtitleStream);
Assert.Equal(13d, res.VideoStream.Level);
Assert.Equal("4", res.VideoStream.NalLengthSize);
Assert.Equal("yuv444p", res.VideoStream.PixelFormat);
Assert.Equal("High 4:4:4 Predictive", res.VideoStream.Profile);
Assert.Equal(25f, res.VideoStream.RealFrameRate);
Assert.Equal(1, res.VideoStream.RefFrames);
Assert.Equal("1/1000", res.VideoStream.TimeBase);
Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
Assert.Empty(res.Chapters);
Assert.Equal("Just color bars", res.Overview);
}
[Fact]
public void GetMediaInfo_MusicVideo_Success()
{
var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/music_video.mkv", MediaProtocol.File);
Assert.Equal("The Title", res.Name);
Assert.Equal("Title, The", res.ForcedSortName);
Assert.Single(res.Artists);
Assert.Equal("The Artist", res.Artists[0]);
Assert.Equal("Album", res.Album);
Assert.Equal(2021, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
}
}
}

View File

@@ -1,96 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.MediaInfo;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests
{
public class SsaParserTests
{
// commonly shared invariant value between tests, assumes default format order
private const string InvariantDialoguePrefix = "[Events]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,";
private SsaParser parser = new SsaParser();
[Theory]
[InlineData("[EvEnTs]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,text", "text")] // label casing insensitivity
[InlineData("[Events]\n,0:00:00.00,0:00:00.01,,,,,,,labelless dialogue", "labelless dialogue")] // no "Dialogue:" label, it is optional
[InlineData("[Events]\nFormat: Text, Start, End, Layer, Effect, Style\nDialogue: reordered text,0:00:00.00,0:00:00.01", "reordered text")] // reordered formats
[InlineData(InvariantDialoguePrefix + "Cased TEXT", "Cased TEXT")] // preserve text casing
[InlineData(InvariantDialoguePrefix + " text ", " text ")] // do not trim text
[InlineData(InvariantDialoguePrefix + "text, more text", "text, more text")] // append excess dialogue values (> 10) to text
[InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text{\\fn} end", "start <font face=\"Font Name\">text</font> end")] // font name
[InlineData(InvariantDialoguePrefix + "start {\\fs10}text{\\fs} end", "start <font size=\"10\">text</font> end")] // font size
[InlineData(InvariantDialoguePrefix + "start {\\c&H112233}text{\\c} end", "start <font color=\"#332211\">text</font> end")] // color
[InlineData(InvariantDialoguePrefix + "start {\\1c&H112233}text{\\1c} end", "start <font color=\"#332211\">text</font> end")] // primay color
[InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text1 {\\fs10}text2{\\fs}{\\fn} {\\1c&H112233}text3{\\1c} end", "start <font face=\"Font Name\">text1 <font size=\"10\">text2</font></font> <font color=\"#332211\">text3</font> end")] // nested formatting
public void Parse(string ssa, string expectedText)
{
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
{
SubtitleTrackInfo subtitleTrackInfo = parser.Parse(stream, CancellationToken.None);
SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[0];
Assert.Equal(expectedText, actual.Text);
}
}
[Theory]
[MemberData(nameof(Parse_MultipleDialogues_TestData))]
public void Parse_MultipleDialogues(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
{
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
{
SubtitleTrackInfo subtitleTrackInfo = parser.Parse(stream, CancellationToken.None);
Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
{
SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
Assert.Equal(expected.Text, actual.Text);
}
}
}
public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
{
yield return new object[]
{
@"[Events]
Format: Layer, Start, End, Text
Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
Dialogue: ,0:00:02.18,0:00:02.85,dialogue2
Dialogue: ,0:00:03.18,0:00:03.85,dialogue3
",
new List<SubtitleTrackEvent>
{
new SubtitleTrackEvent
{
StartPositionTicks = 11800000,
EndPositionTicks = 18500000,
Text = "dialogue1"
},
new SubtitleTrackEvent
{
StartPositionTicks = 21800000,
EndPositionTicks = 28500000,
Text = "dialogue2"
},
new SubtitleTrackEvent
{
StartPositionTicks = 31800000,
EndPositionTicks = 38500000,
Text = "dialogue3"
}
}
};
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Globalization;
using System.IO;
using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.MediaEncoding.Subtitles.Tests
@@ -14,25 +15,15 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (var stream = File.OpenRead("Test Data/example.ass"))
{
var parsed = new AssParser().Parse(stream, CancellationToken.None);
var parsed = new AssParser(new NullLogger<AssParser>()).Parse(stream, CancellationToken.None);
Assert.Single(parsed.TrackEvents);
var trackEvent = parsed.TrackEvents[0];
Assert.Equal("1", trackEvent.Id);
Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
Assert.Equal("Like an Angel with pity on nobody\r\nThe second line in subtitle", trackEvent.Text);
Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text);
}
}
[Fact]
public void ParseFieldHeaders_Valid_Success()
{
const string Line = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text";
var headers = AssParser.ParseFieldHeaders(Line);
Assert.Equal(1, headers["Start"]);
Assert.Equal(2, headers["End"]);
Assert.Equal(9, headers["Text"]);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal("1", trackEvent1.Id);
Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
Assert.Equal("Senator, we're making\r\nour final approach into Coruscant.", trackEvent1.Text);
Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text);
var trackEvent2 = parsed.TrackEvents[1];
Assert.Equal("2", trackEvent2.Id);

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
public class SsaParserTests
{
private readonly SsaParser _parser = new SsaParser(new NullLogger<AssParser>());
[Theory]
[MemberData(nameof(Parse_MultipleDialogues_TestData))]
public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
{
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
{
SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, CancellationToken.None);
Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
{
SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
Assert.Equal(expected.Id, actual.Id);
Assert.Equal(expected.Text, actual.Text);
Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
}
}
}
public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
{
yield return new object[]
{
@"[Events]
Format: Layer, Start, End, Text
Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
Dialogue: ,0:00:02.18,0:00:02.85,dialogue2
Dialogue: ,0:00:03.18,0:00:03.85,dialogue3
",
new List<SubtitleTrackEvent>
{
new SubtitleTrackEvent("1", "dialogue1")
{
StartPositionTicks = 11800000,
EndPositionTicks = 18500000
},
new SubtitleTrackEvent("2", "dialogue2")
{
StartPositionTicks = 21800000,
EndPositionTicks = 28500000
},
new SubtitleTrackEvent("3", "dialogue3")
{
StartPositionTicks = 31800000,
EndPositionTicks = 38500000
}
}
};
}
[Fact]
public void Parse_Valid_Success()
{
using (var stream = File.OpenRead("Test Data/example.ssa"))
{
var parsed = _parser.Parse(stream, CancellationToken.None);
Assert.Single(parsed.TrackEvents);
var trackEvent = parsed.TrackEvents[0];
Assert.Equal("1", trackEvent.Id);
Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text);
}
}
}
}

View File

@@ -0,0 +1,111 @@
{
"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": "1001/48000",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1088,
"closed_captions": 0,
"has_b_frames": 0,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p",
"level": 42,
"chroma_location": "left",
"field_order": "progressive",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"r_frame_rate": "24000/1001",
"avg_frame_rate": "24000/1001",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"bits_per_raw_sample": "8",
"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": "eng"
}
},
{
"index": 1,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"sample_fmt": "fltp",
"sample_rate": "48000",
"channels": 2,
"channel_layout": "stereo",
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"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": "eng"
}
}
],
"chapters": [
],
"format": {
"filename": "music_video.mkv",
"nb_streams": 2,
"nb_programs": 0,
"format_name": "matroska,webm",
"format_long_name": "Matroska / WebM",
"start_time": "0.000000",
"duration": "180.000000",
"size": "500000000",
"bit_rate": "22222222",
"probe_score": 100,
"tags": {
"TITLE-eng": "The Title",
"TITLESORT": "Title, The",
"ARTIST": "The Artist",
"ARTISTSORT": "Artist, The",
"ALBUM": "Album",
"DATE_RELEASED": "2021-01-01"
}
}
}

View File

@@ -0,0 +1,74 @@
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High 4:4:4 Predictive",
"codec_type": "video",
"codec_time_base": "1/50",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"width": 320,
"height": 240,
"coded_width": 320,
"coded_height": 240,
"closed_captions": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "4:3",
"pix_fmt": "yuv444p",
"level": 13,
"chroma_location": "left",
"field_order": "progressive",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"r_frame_rate": "25/1",
"avg_frame_rate": "25/1",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"bits_per_raw_sample": "8",
"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": {
"ENCODER": "Lavc57.107.100 libx264",
"DURATION": "00:00:01.000000000"
}
}
],
"chapters": [
],
"format": {
"filename": "some_metadata.mkv",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "matroska,webm",
"format_long_name": "Matroska / WebM",
"start_time": "0.000000",
"duration": "1.000000",
"size": "8679",
"bit_rate": "69432",
"probe_score": 100,
"tags": {
"DESCRIPTION": "Just color bars",
"ARCHIVAL": "yes",
"PRESERVE_THIS": "okay",
"ENCODER": "Lavf57.83.100"
}
}
}

View File

@@ -0,0 +1,20 @@
[Script Info]
; This is a Sub Station Alpha v4 script.
; For Sub Station Alpha info and downloads,
; go to http://www.eswat.demon.co.uk/
Title: Neon Genesis Evangelion - Episode 26 (neutral Spanish)
Original Script: RoRo
Script Updated By: version 2.8.01
ScriptType: v4.00
Collisions: Normal
PlayResY: 600
PlayDepth: 0
Timer: 100,0000
[V4 Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
Style: DefaultVCD, Arial,28,11861244,11861244,11861244,-2147483640,-1,0,1,1,2,2,30,30,30,0,0
[Events]
Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: Marked=0,0:00:01.18,0:00:06.85,DefaultVCD, NTP,0000,0000,0000,,{\pos(400,570)}Like an angel with pity on nobody

View File

@@ -0,0 +1,19 @@
using MediaBrowser.Model.Dlna;
using Xunit;
namespace Jellyfin.Model.Tests.Dlna
{
public class ContainerProfileTests
{
private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile();
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("mp4")]
public void ContainsContainer_EmptyContainerProfile_True(string? containers)
{
Assert.True(_emptyContainerProfile.ContainsContainer(containers));
}
}
}

View File

@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Entities;
using Xunit;
namespace Jellyfin.Model.Tests.Entities
{
public class JsonLowerCaseConverterTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
{
Converters =
{
new JsonStringEnumConverter()
}
};
[Theory]
[InlineData(null, "{\"CollectionType\":null}")]
[InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")]
[InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")]
public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected)
{
Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions));
}
[Theory]
[InlineData("{\"CollectionType\":null}", null)]
[InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)]
[InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)]
public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result)
{
var res = JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions);
Assert.NotNull(res);
Assert.Equal(result, res!.CollectionType);
}
[Theory]
[InlineData(null)]
[InlineData(CollectionTypeOptions.Movies)]
[InlineData(CollectionTypeOptions.MusicVideos)]
public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value)
{
var res = JsonSerializer.Deserialize<TestContainer>(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions);
Assert.NotNull(res);
Assert.Equal(value, res!.CollectionType);
}
[Theory]
[InlineData("{\"CollectionType\":null}")]
[InlineData("{\"CollectionType\":\"movies\"}")]
[InlineData("{\"CollectionType\":\"musicvideos\"}")]
public void RoundTrip_String_Correct(string json)
{
var res = JsonSerializer.Serialize(JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions), _jsonOptions);
Assert.Equal(json, res);
}
private class TestContainer
{
public TestContainer(CollectionTypeOptions? collectionType)
{
CollectionType = collectionType;
}
[JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
public CollectionTypeOptions? CollectionType { get; set; }
}
}
}

View File

@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
using Xunit;
namespace Jellyfin.Model.Tests.Entities
{
public class ProviderIdsExtensionsTests
{
private const string ExampleImdbId = "tt0113375";
[Fact]
public void HasProviderId_NullInstance_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.HasProviderId(null!, MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_NullProvider_False()
{
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
Assert.False(nullProvider.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_NullName_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.HasProviderId(null!));
}
[Fact]
public void HasProviderId_NotFoundName_False()
{
Assert.False(ProviderIdsExtensionsTestsObject.Empty.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_FoundName_True()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
Assert.True(provider.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_FoundNameEmptyValue_False()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty;
Assert.False(provider.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void GetProviderId_NullInstance_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.GetProviderId(null!, MetadataProvider.Imdb));
}
[Fact]
public void GetProviderId_NullName_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.GetProviderId(null!));
}
[Fact]
public void GetProviderId_NotFoundName_Null()
{
Assert.Null(ProviderIdsExtensionsTestsObject.Empty.GetProviderId(MetadataProvider.Imdb));
}
[Fact]
public void GetProviderId_NullProvider_Null()
{
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
Assert.Null(nullProvider.GetProviderId(MetadataProvider.Imdb));
}
[Fact]
public void TryGetProviderId_NotFoundName_False()
{
Assert.False(ProviderIdsExtensionsTestsObject.Empty.TryGetProviderId(MetadataProvider.Imdb, out _));
}
[Fact]
public void TryGetProviderId_NullProvider_False()
{
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
Assert.False(nullProvider.TryGetProviderId(MetadataProvider.Imdb, out _));
}
[Fact]
public void GetProviderId_FoundName_Id()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
Assert.Equal(ExampleImdbId, provider.GetProviderId(MetadataProvider.Imdb));
}
[Fact]
public void TryGetProviderId_FoundName_True()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
Assert.True(provider.TryGetProviderId(MetadataProvider.Imdb, out var id));
Assert.Equal(ExampleImdbId, id);
}
[Fact]
public void TryGetProviderId_FoundNameEmptyValue_False()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty;
Assert.False(provider.TryGetProviderId(MetadataProvider.Imdb, out var id));
Assert.Null(id);
}
[Fact]
public void SetProviderId_NullInstance_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.SetProviderId(null!, MetadataProvider.Imdb, ExampleImdbId));
}
[Fact]
public void SetProviderId_Null_Remove()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.SetProviderId(MetadataProvider.Imdb, null!);
Assert.Empty(provider.ProviderIds);
}
[Fact]
public void SetProviderId_EmptyName_Remove()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
provider.SetProviderId(MetadataProvider.Imdb, string.Empty);
Assert.Empty(provider.ProviderIds);
}
[Fact]
public void SetProviderId_NonEmptyId_Success()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.SetProviderId(MetadataProvider.Imdb, ExampleImdbId);
Assert.Single(provider.ProviderIds);
}
[Fact]
public void SetProviderId_NullProvider_Success()
{
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
nullProvider.SetProviderId(MetadataProvider.Imdb, ExampleImdbId);
Assert.Single(nullProvider.ProviderIds);
}
[Fact]
public void SetProviderId_NullProviderAndEmptyName_Success()
{
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
nullProvider.SetProviderId(MetadataProvider.Imdb, string.Empty);
Assert.Null(nullProvider.ProviderIds);
}
private class ProviderIdsExtensionsTestsObject : IHasProviderIds
{
public static readonly ProviderIdsExtensionsTestsObject Empty = new ProviderIdsExtensionsTestsObject();
public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
}
}
}

View File

@@ -1,4 +1,6 @@
using System;
using FsCheck;
using FsCheck.Xunit;
using MediaBrowser.Model.Extensions;
using Xunit;
@@ -11,9 +13,20 @@ namespace Jellyfin.Model.Tests.Extensions
[InlineData("banana", "Banana")]
[InlineData("Banana", "Banana")]
[InlineData("ä", "Ä")]
[InlineData("\027", "\027")]
public void StringHelper_ValidArgs_Success(string input, string expectedResult)
{
Assert.Equal(expectedResult, StringHelper.FirstToUpper(input));
}
[Property]
public Property FirstToUpper_RandomArg_Correct(NonEmptyString input)
{
var result = StringHelper.FirstToUpper(input.Item);
// We check IsLower instead of IsUpper because both return false for non-letters
return (!char.IsLower(result[0])).Label("First char is uppercase")
.And(input.Item.Length == 1 || result[1..].Equals(input.Item[1..], StringComparison.Ordinal)).Label("Remaining chars are unmodified");
}
}
}

View File

@@ -5,18 +5,20 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="1.2.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -26,8 +28,4 @@
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
@@ -10,7 +9,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
private readonly NamingOptions _namingOptions = new NamingOptions();
public static IEnumerable<object[]> GetResolveFileTestData()
public static IEnumerable<object[]> Resolve_ValidFileNameTestData()
{
yield return new object[]
{
@@ -36,7 +35,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
}
[Theory]
[MemberData(nameof(GetResolveFileTestData))]
[MemberData(nameof(Resolve_ValidFileNameTestData))]
public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult)
{
var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path);

View File

@@ -8,15 +8,17 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<ItemGroup>
@@ -25,14 +27,9 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -1,4 +1,3 @@
using System;
using Emby.Naming.Common;
using Emby.Naming.Subtitles;
using Xunit;

View File

@@ -58,7 +58,7 @@ namespace Jellyfin.Naming.Tests.Video
{
input = Path.GetFileName(input);
var result = new VideoResolver(_namingOptions).CleanDateTime(input);
var result = VideoResolver.CleanDateTime(input, _namingOptions);
Assert.Equal(expectedName, result.Name, true);
Assert.Equal(expectedYear, result.Year);

View File

@@ -14,11 +14,6 @@ namespace Jellyfin.Naming.Tests.Video
[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")]
[InlineData("Super movie(2009).mp4", "Super movie(2009).mp4")]
[InlineData("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4")]
[InlineData(@"American.Psycho.mkv", "American.Psycho.mkv")]
[InlineData(@"American Psycho.mkv", "American Psycho.mkv")]
[InlineData(@"[rec].mkv", "[rec].mkv")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon")]
@@ -29,17 +24,25 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest(string input, string expectedName)
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
if (new VideoResolver(_namingOptions).TryCleanString(input, out ReadOnlySpan<char> newName))
{
// TODO: compare spans when XUnit supports it
Assert.Equal(expectedName, newName.ToString());
}
else
{
Assert.Equal(expectedName, input);
}
Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
// TODO: compare spans when XUnit supports it
Assert.Equal(expectedName, newName.ToString());
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("Super movie(2009).mp4")]
[InlineData("[rec].mkv")]
[InlineData("American.Psycho.mkv")]
[InlineData("American Psycho.mkv")]
[InlineData("Run lola run (lola rennt) (2009).mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
Assert.True(newName.IsEmpty);
}
}
}

View File

@@ -1,4 +1,3 @@
using System;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
@@ -105,13 +104,6 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(rule, res.Rule);
}
[Fact]
public void TestFlagsParser()
{
var flags = new FlagParser(_videoOptions).GetFlags(string.Empty);
Assert.Empty(flags);
}
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);

View File

@@ -22,8 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void Test3DName()
{
var result =
new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
Assert.Equal("hsbs", result?.Format3D);
Assert.Equal("Oblivion", result?.Name);
@@ -58,15 +57,13 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, bool is3D, string? format3D)
{
var parser = new Format3DParser(_namingOptions);
var result = parser.Parse(input);
var result = Format3DParser.Parse(input, _namingOptions);
Assert.Equal(is3D, result.Is3D);
if (format3D == null)
{
Assert.Null(result.Format3D);
Assert.Null(result?.Format3D);
}
else
{

View File

@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class MultiVersionTests
{
private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions());
private readonly NamingOptions _namingOptions = new NamingOptions();
[Fact]
public void TestMultiEdition1()
@@ -22,11 +22,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -43,11 +45,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -63,11 +67,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -87,11 +93,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/M/Movie 7.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
@@ -113,11 +121,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie-8.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Mo/Movie 9.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(9, result.Count);
Assert.Empty(result[0].Extras);
@@ -163,11 +175,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie 5.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -188,11 +202,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -214,11 +230,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man[test].mkv",
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -243,11 +261,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man [test].mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -266,11 +286,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man - C (2007).mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -289,18 +311,17 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
Assert.Equal(6, 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.Empty(result[0].AlternateVersions);
}
[Fact]
@@ -317,11 +338,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -337,11 +360,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -357,21 +382,65 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
[Fact]
public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
{
var files = new[]
{
@"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
@"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
[Fact]
public void Resolve_GivenUnclosedBrackets_DoesNotGroup()
{
var files = new[]
{
@"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
@"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
}
[Fact]
public void TestEmptyList()
{
var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList();
var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList();
Assert.Empty(result);
}

View File

@@ -29,8 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestStubName()
{
var result =
new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
Assert.Equal("Oblivion", result?.Name);
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -10,9 +11,8 @@ namespace Jellyfin.Naming.Tests.Video
{
private readonly NamingOptions _namingOptions = new NamingOptions();
// FIXME
// [Fact]
private void TestStackAndExtras()
[Fact]
public void TestStackAndExtras()
{
// No stacking here because there is no part/disc/etc
var files = new[]
@@ -40,23 +40,24 @@ namespace Jellyfin.Naming.Tests.Video
"WillyWonka-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
Assert.NotNull(batman);
Assert.Equal(3, batman!.Files.Count);
Assert.Equal(3, batman!.Extras.Count);
Assert.Equal(3, result[1].Files.Count);
Assert.Equal(3, result[1].Extras.Count);
Assert.Equal("Batman", result[1].Name);
Assert.Equal(4, result[2].Files.Count);
Assert.Equal(2, result[2].Extras.Count);
Assert.Equal("Harry Potter and the Deathly Hallows", result[2].Name);
var harry = result.FirstOrDefault(x => string.Equals(x.Name, "Harry Potter and the Deathly Hallows", StringComparison.Ordinal));
Assert.NotNull(harry);
Assert.Equal(4, harry!.Files.Count);
Assert.Equal(2, harry!.Extras.Count);
}
[Fact]
@@ -68,13 +69,13 @@ namespace Jellyfin.Naming.Tests.Video
"300.nfo"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -88,13 +89,13 @@ namespace Jellyfin.Naming.Tests.Video
"300 trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -108,13 +109,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -129,13 +130,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer2.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -149,13 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
"Looper.2012.bluray.720p.x264.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -173,13 +174,13 @@ namespace Jellyfin.Naming.Tests.Video
"My video 5.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
}
@@ -193,13 +194,13 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = true,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = true,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -214,13 +215,13 @@ namespace Jellyfin.Naming.Tests.Video
@"My movie #2.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = true,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = true,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -235,13 +236,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012) part1-trailer.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -256,13 +257,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012)-trailer.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -278,13 +279,13 @@ namespace Jellyfin.Naming.Tests.Video
@"trailer.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -300,13 +301,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -319,13 +320,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -338,13 +339,13 @@ namespace Jellyfin.Naming.Tests.Video
@"The Colony.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -358,13 +359,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Sisters and a Wedding - B.avi"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -378,13 +379,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Rooms - A.mp4"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -398,13 +399,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/movie-trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -420,13 +421,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Equal(4, result.Count);
}
@@ -440,13 +441,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Despicable Me/trailers/trailer.mkv"
};
var resolver = GetResolver();
var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
var result = VideoListResolver.Resolve(
files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList(),
_namingOptions).ToList();
Assert.Single(result);
}
@@ -457,10 +458,5 @@ namespace Jellyfin.Naming.Tests.Video
var stack = new FileStack();
Assert.False(stack.ContainsFile("XX", true));
}
private VideoListResolver GetResolver()
{
return new VideoListResolver(_namingOptions);
}
}
}

View File

@@ -9,9 +9,9 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoResolverTests
{
private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
private static NamingOptions _namingOptions = new NamingOptions();
public static IEnumerable<object[]> GetResolveFileTestData()
public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
{
yield return new object[]
{
@@ -148,7 +148,7 @@ namespace Jellyfin.Naming.Tests.Video
yield return new object[]
{
new VideoFileInfo(
path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4",
path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
container: "mp4",
name: "Rain Man",
year: 1988)
@@ -156,30 +156,30 @@ namespace Jellyfin.Naming.Tests.Video
}
[Theory]
[MemberData(nameof(GetResolveFileTestData))]
[MemberData(nameof(ResolveFile_ValidFileNameTestData))]
public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
{
var result = _videoResolver.ResolveFile(expectedResult.Path);
var result = VideoResolver.ResolveFile(expectedResult.Path, _namingOptions);
Assert.NotNull(result);
Assert.Equal(result?.Path, expectedResult.Path);
Assert.Equal(result?.Container, expectedResult.Container);
Assert.Equal(result?.Name, expectedResult.Name);
Assert.Equal(result?.Year, expectedResult.Year);
Assert.Equal(result?.ExtraType, expectedResult.ExtraType);
Assert.Equal(result?.Format3D, expectedResult.Format3D);
Assert.Equal(result?.Is3D, expectedResult.Is3D);
Assert.Equal(result?.IsStub, expectedResult.IsStub);
Assert.Equal(result?.StubType, expectedResult.StubType);
Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
Assert.Equal(result?.ToString(), expectedResult.ToString());
Assert.Equal(result!.Path, expectedResult.Path);
Assert.Equal(result.Container, expectedResult.Container);
Assert.Equal(result.Name, expectedResult.Name);
Assert.Equal(result.Year, expectedResult.Year);
Assert.Equal(result.ExtraType, expectedResult.ExtraType);
Assert.Equal(result.Format3D, expectedResult.Format3D);
Assert.Equal(result.Is3D, expectedResult.Is3D);
Assert.Equal(result.IsStub, expectedResult.IsStub);
Assert.Equal(result.StubType, expectedResult.StubType);
Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
Assert.Equal(result.FileNameWithoutExtension.ToString(), expectedResult.FileNameWithoutExtension.ToString());
Assert.Equal(result.ToString(), expectedResult.ToString());
}
[Fact]
public void ResolveFile_EmptyPath()
{
var result = _videoResolver.ResolveFile(string.Empty);
var result = VideoResolver.ResolveFile(string.Empty, _namingOptions);
Assert.Null(result);
}
@@ -194,12 +194,16 @@ namespace Jellyfin.Naming.Tests.Video
string.Empty
};
var results = paths.Select(path => _videoResolver.ResolveDirectory(path)).ToList();
var results = paths.Select(path => VideoResolver.ResolveDirectory(path, _namingOptions)).ToList();
Assert.Equal(3, results.Count);
Assert.NotNull(results[0]);
Assert.NotNull(results[1]);
Assert.Null(results[2]);
foreach (var result in results)
{
Assert.Null(result?.Container);
}
}
}
}

View File

@@ -0,0 +1,53 @@
using FsCheck;
using FsCheck.Xunit;
using MediaBrowser.Common.Net;
using Xunit;
namespace Jellyfin.Networking.Tests
{
public static class IPHostTests
{
/// <summary>
/// Checks IP address formats.
/// </summary>
/// <param name="address">IP Address.</param>
[Theory]
[InlineData("127.0.0.1")]
[InlineData("127.0.0.1:123")]
[InlineData("localhost")]
[InlineData("localhost:1345")]
[InlineData("www.google.co.uk")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
[InlineData("fe80::7add:12ff:febb:c67b%16")]
[InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
[InlineData("fe80::7add:12ff:febb:c67b%16:123")]
[InlineData("[fe80::7add:12ff:febb:c67b%16]")]
[InlineData("192.168.1.2/255.255.255.0")]
[InlineData("192.168.1.2/24")]
public static void TryParse_ValidHostStrings_True(string address)
=> Assert.True(IPHost.TryParse(address, out _));
[Property]
public static Property TryParse_IPv4Address_True(IPv4Address address)
=> IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
[Property]
public static Property TryParse_IPv6Address_True(IPv6Address address)
=> IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
/// <summary>
/// All should be invalid address strings.
/// </summary>
/// <param name="address">Invalid address strings.</param>
[Theory]
[InlineData("256.128.0.0.0.1")]
[InlineData("127.0.0.1#")]
[InlineData("localhost!")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
public static void TryParse_InvalidAddressString_False(string address)
=> Assert.False(IPHost.TryParse(address, out _));
}
}

View File

@@ -0,0 +1,49 @@
using FsCheck;
using FsCheck.Xunit;
using MediaBrowser.Common.Net;
using Xunit;
namespace Jellyfin.Networking.Tests
{
public static class IPNetAddressTests
{
/// <summary>
/// Checks IP address formats.
/// </summary>
/// <param name="address">IP Address.</param>
[Theory]
[InlineData("127.0.0.1")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
[InlineData("fe80::7add:12ff:febb:c67b%16")]
[InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
[InlineData("fe80::7add:12ff:febb:c67b%16:123")]
[InlineData("[fe80::7add:12ff:febb:c67b%16]")]
[InlineData("192.168.1.2/255.255.255.0")]
[InlineData("192.168.1.2/24")]
public static void TryParse_ValidIPStrings_True(string address)
=> Assert.True(IPNetAddress.TryParse(address, out _));
[Property]
public static Property TryParse_IPv4Address_True(IPv4Address address)
=> IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
[Property]
public static Property TryParse_IPv6Address_True(IPv6Address address)
=> IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
/// <summary>
/// All should be invalid address strings.
/// </summary>
/// <param name="address">Invalid address strings.</param>
[Theory]
[InlineData("256.128.0.0.0.1")]
[InlineData("127.0.0.1#")]
[InlineData("localhost!")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
public static void TryParse_InvalidAddressString_False(string address)
=> Assert.False(IPNetAddress.TryParse(address, out _));
}
}

View File

@@ -8,32 +8,35 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="../../Emby.Server.Implementations/Emby.Server.Implementations.csproj" />
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,63 @@
using System.Net;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.Networking.Tests
{
public class NetworkManagerTests
{
/// <summary>
/// Checks that the given IP address is in the specified network(s).
/// </summary>
/// <param name="network">Network address(es).</param>
/// <param name="value">The IP to check.</param>
[Theory]
[InlineData("192.168.2.1/24", "192.168.2.123")]
[InlineData("192.168.2.1/24, !192.168.2.122/32", "192.168.2.123")]
[InlineData("fd23:184f:2029:0::/56", "fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d518/128", "fd23:184f:2029:0:3139:7386:67d7:d517")]
public void InNetwork_True_Success(string network, string value)
{
var ip = IPAddress.Parse(value);
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
EnableIPV4 = true,
LocalNetworkSubnets = network.Split(',')
};
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
Assert.True(networkManager.IsInLocalNetwork(ip));
}
/// <summary>
/// Checks that thge given IP address is not in the network provided.
/// </summary>
/// <param name="network">Network address(es).</param>
/// <param name="value">The IP to check.</param>
[Theory]
[InlineData("192.168.10.0/24", "192.168.11.1")]
[InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")]
[InlineData("192.168.10.0/24", "fd23:184f:2029:0:3139:7386:67d7:d517")]
[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")]
public void InNetwork_False_Success(string network, string value)
{
var ip = IPAddress.Parse(value);
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
EnableIPV4 = true,
LocalNetworkSubnets = network.Split(',')
};
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>());
Assert.False(nm.IsInLocalNetwork(ip));
}
}
}

View File

@@ -1,47 +1,19 @@
using System;
using System.Collections.ObjectModel;
using System.Net;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using Moq;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using System.Collections.ObjectModel;
namespace Jellyfin.Networking.Tests
{
public class NetworkParseTests
{
/// <summary>
/// Tries to identify the string and return an object of that class.
/// </summary>
/// <param name="addr">String to parse.</param>
/// <param name="result">IPObject to return.</param>
/// <returns>True if the value parsed successfully.</returns>
private static bool TryParse(string addr, out IPObject result)
{
if (!string.IsNullOrEmpty(addr))
{
// Is it an IP address
if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
{
result = nw;
return true;
}
if (IPHost.TryParse(addr, out IPHost h))
{
result = h;
return true;
}
}
result = IPNetAddress.None;
return false;
}
private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
internal static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
{
var configManager = new Mock<IConfigurationManager>
{
@@ -52,15 +24,22 @@ namespace Jellyfin.Networking.Tests
}
/// <summary>
/// Checks the ability to ignore interfaces
/// Checks the ability to ignore virtual interfaces.
/// </summary>
/// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) | .... </param>
/// <param name="lan">LAN addresses.</param>
/// <param name="value">Bind addresses that are excluded.</param>
[Theory]
// All valid
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
// eth16 only
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
[InlineData("192.168.1.208/24,-16,vEthernet1|192.168.1.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
// All interfaces excluded. (including loopbacks)
[InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[127.0.0.1/8,::1/128]")]
// vEthernet1 and vEthernet212 should be excluded.
[InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24,127.0.0.1/8,::1/128]")]
// Overlapping interface,
[InlineData("192.168.1.110/24,-20,br0|192.168.1.10/24,-16,br0|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.110/24,192.168.1.10/24]")]
public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
{
var conf = new NetworkConfiguration()
@@ -77,71 +56,6 @@ namespace Jellyfin.Networking.Tests
Assert.Equal(nm.GetInternalBindAddresses().AsString(), value);
}
/// <summary>
/// Check that the value given is in the network provided.
/// </summary>
/// <param name="network">Network address.</param>
/// <param name="value">Value to check.</param>
[Theory]
[InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")]
public void IsInNetwork(string network, string value)
{
if (network == null)
{
throw new ArgumentNullException(nameof(network));
}
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
EnableIPV4 = true,
LocalNetworkSubnets = network.Split(',')
};
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
Assert.False(nm.IsInLocalNetwork(value));
}
/// <summary>
/// Checks IP address formats.
/// </summary>
/// <param name="address"></param>
[Theory]
[InlineData("127.0.0.1")]
[InlineData("127.0.0.1:123")]
[InlineData("localhost")]
[InlineData("localhost:1345")]
[InlineData("www.google.co.uk")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
[InlineData("fe80::7add:12ff:febb:c67b%16")]
[InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
[InlineData("192.168.1.2/255.255.255.0")]
[InlineData("192.168.1.2/24")]
public void ValidIPStrings(string address)
{
Assert.True(TryParse(address, out _));
}
/// <summary>
/// All should be invalid address strings.
/// </summary>
/// <param name="address">Invalid address strings.</param>
[Theory]
[InlineData("256.128.0.0.0.1")]
[InlineData("127.0.0.1#")]
[InlineData("localhost!")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
public void InvalidAddressString(string address)
{
Assert.False(TryParse(address, out _));
}
/// <summary>
/// Test collection parsing.
/// </summary>
@@ -152,19 +66,22 @@ namespace Jellyfin.Networking.Tests
/// <param name="result4">Excluded IP4 addresses from the collection.</param>
/// <param name="result5">Network addresses of the collection.</param>
[Theory]
[InlineData("127.0.0.1#",
[InlineData(
"127.0.0.1#",
"[]",
"[]",
"[]",
"[]",
"[]")]
[InlineData("!127.0.0.1",
[InlineData(
"!127.0.0.1",
"[]",
"[]",
"[127.0.0.1/32]",
"[127.0.0.1/32]",
"[]")]
[InlineData("",
[InlineData(
"",
"[]",
"[]",
"[]",
@@ -172,12 +89,13 @@ namespace Jellyfin.Networking.Tests
"[]")]
[InlineData(
"192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, !10.10.10.10",
"[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]",
"[192.158.1.2/16,[127.0.0.1/32,::1/128],fd23:184f:2029:0:3139:7386:67d7:d517/128]",
"[192.158.1.2/16,127.0.0.1/32]",
"[10.10.10.10/32]",
"[10.10.10.10/32]",
"[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
[InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8",
"[192.158.0.0/16,127.0.0.1/32,::1/128,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
[InlineData(
"192.158.1.2/255.255.0.0,192.169.1.2/8",
"[192.158.1.2/16,192.169.1.2/8]",
"[192.158.1.2/16,192.169.1.2/8]",
"[]",
@@ -194,34 +112,34 @@ namespace Jellyfin.Networking.Tests
{
EnableIPV6 = true,
EnableIPV4 = true,
};
};
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
// Test included.
Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false);
Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(','), false);
Assert.Equal(nc.AsString(), result1);
// Test excluded.
nc = nm.CreateIPCollection(settings.Split(","), true);
nc = nm.CreateIPCollection(settings.Split(','), true);
Assert.Equal(nc.AsString(), result3);
conf.EnableIPV6 = false;
nm.UpdateSettings(conf);
// Test IP4 included.
nc = nm.CreateIPCollection(settings.Split(","), false);
nc = nm.CreateIPCollection(settings.Split(','), false);
Assert.Equal(nc.AsString(), result2);
// Test IP4 excluded.
nc = nm.CreateIPCollection(settings.Split(","), true);
nc = nm.CreateIPCollection(settings.Split(','), true);
Assert.Equal(nc.AsString(), result4);
conf.EnableIPV6 = true;
nm.UpdateSettings(conf);
// Test network addresses of collection.
nc = nm.CreateIPCollection(settings.Split(","), false);
nc = nm.CreateIPCollection(settings.Split(','), false);
nc = nc.AsNetworks();
Assert.Equal(nc.AsString(), result5);
}
@@ -252,7 +170,6 @@ namespace Jellyfin.Networking.Tests
throw new ArgumentNullException(nameof(result));
}
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
@@ -261,10 +178,10 @@ namespace Jellyfin.Networking.Tests
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false);
Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false);
Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(','), false);
Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(','), false);
Assert.Equal(nc1.Union(nc2).AsString(), result);
Assert.Equal(nc1.ThatAreContainedInNetworks(nc2).AsString(), result);
}
[Theory]
@@ -333,8 +250,8 @@ namespace Jellyfin.Networking.Tests
public void TestSubnetContains(string network, string ip)
{
Assert.True(TryParse(network, out IPObject? networkObj));
Assert.True(TryParse(ip, out IPObject? ipObj));
Assert.True(IPNetAddress.TryParse(network, out var networkObj));
Assert.True(IPNetAddress.TryParse(ip, out var ipObj));
Assert.True(networkObj.Contains(ipObj));
}
@@ -371,14 +288,13 @@ namespace Jellyfin.Networking.Tests
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
// Test included, IP6.
Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(","));
Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(","));
Collection<IPObject> ncResult = ncSource.Union(ncDest);
Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(","));
Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(','));
Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(','));
Collection<IPObject> ncResult = ncSource.ThatAreContainedInNetworks(ncDest);
Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(','));
Assert.True(ncResult.Compare(resultCollection));
}
[Theory]
[InlineData("10.1.1.1/32", "10.1.1.1")]
[InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")]
@@ -408,6 +324,9 @@ namespace Jellyfin.Networking.Tests
[InlineData("jellyfin.org", "eth16", false, "eth16")]
// User on external network, no binding - so result is the 1st external.
[InlineData("jellyfin.org", "", false, "eth11")]
// Dns failure - should skip the test.
// https://en.wikipedia.org/wiki/.test
[InlineData("invalid.domain.test", "", false, "eth11")]
// User assumed to be internal, no binding - so result is the 1st internal.
[InlineData("", "", false, "eth16")]
public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result)
@@ -440,10 +359,13 @@ namespace Jellyfin.Networking.Tests
_ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj);
if (resultObj != null)
// Check to see if dns resolution is working. If not, skip test.
_ = IPHost.TryParse(source, out var host);
if (resultObj != null && host?.HasAddress == true)
{
result = ((IPNetAddress)resultObj[0]).ToString(true);
var intf = nm.GetBindInterface(source, out int? _);
var intf = nm.GetBindInterface(source, out _);
Assert.Equal(intf, result);
}
@@ -455,7 +377,7 @@ namespace Jellyfin.Networking.Tests
// On my system eth16 is internal, eth11 external (Windows defines the indexes).
//
// This test is to replicate how subnet bound ServerPublisherUri work throughout the system.
// User on internal network, we're bound internal and external - so result is internal override.
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
@@ -468,7 +390,7 @@ namespace Jellyfin.Networking.Tests
// User on internal network, no binding specified - so result is the 1st internal.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
// User on external network, internal binding only - so asumption is a proxy forward, return external override.
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
// User on external network, no binding - so result is the 1st external which is overriden.
@@ -479,7 +401,6 @@ namespace Jellyfin.Networking.Tests
// User is internal, no binding - so result is the 1st internal, which is then overridden.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
{
if (lan == null)
@@ -515,5 +436,45 @@ namespace Jellyfin.Networking.Tests
Assert.Equal(intf, result);
}
[Theory]
[InlineData("185.10.10.10,200.200.200.200", "79.2.3.4", true)]
[InlineData("185.10.10.10", "185.10.10.10", false)]
[InlineData("", "100.100.100.100", false)]
public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIpsInWhitelist(string addresses, string remoteIp, bool denied)
{
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
var conf = new NetworkConfiguration()
{
EnableIPV4 = true,
RemoteIPFilter = addresses.Split(','),
IsRemoteIPFilterBlacklist = false
};
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied);
}
[Theory]
[InlineData("185.10.10.10", "79.2.3.4", false)]
[InlineData("185.10.10.10", "185.10.10.10", true)]
[InlineData("", "100.100.100.100", false)]
public void HasRemoteAccess_GivenBlacklist_BlacklistTheIps(string addresses, string remoteIp, bool denied)
{
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
var conf = new NetworkConfiguration()
{
EnableIPV4 = true,
RemoteIPFilter = addresses.Split(','),
IsRemoteIPFilterBlacklist = true
};
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied);
}
}
}

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,96 @@
#pragma warning disable CA1002 // Do not expose generic lists
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
namespace Jellyfin.Providers.Tests.MediaInfo
{
public class SubtitleResolverTests
{
public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
{
var index = 0;
yield return new object[]
{
new List<MediaStream>(),
"/video/My.Video.mkv",
index,
new[]
{
"/video/My.Video.mp3",
"/video/My.Video.png",
"/video/My.Video.srt",
"/video/My.Video.txt",
"/video/My.Video.vtt",
"/video/My.Video.ass",
"/video/My.Video.sub",
"/video/My.Video.ssa",
"/video/My.Video.smi",
"/video/My.Video.sami",
"/video/My.Video.en.srt",
"/video/My.Video.default.en.srt",
"/video/My.Video.default.forced.en.srt",
"/video/My.Video.en.default.forced.srt",
"/video/My.Video.With.Additional.Garbage.en.srt",
"/video/My.Video With Additional Garbage.srt"
},
new[]
{
CreateMediaStream("/video/My.Video.srt", "srt", null, index++),
CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++),
CreateMediaStream("/video/My.Video.ass", "ass", null, index++),
CreateMediaStream("/video/My.Video.sub", "sub", null, index++),
CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++),
CreateMediaStream("/video/My.Video.smi", "smi", null, index++),
CreateMediaStream("/video/My.Video.sami", "sami", null, index++),
CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++),
CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true),
CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
}
};
}
[Theory]
[MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))]
public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, MediaStream[] expectedResult)
{
new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
Assert.Equal(expectedResult.Length, streams.Count);
for (var i = 0; i < expectedResult.Length; i++)
{
var expected = expectedResult[i];
var actual = streams[i];
Assert.Equal(expected.Index, actual.Index);
Assert.Equal(expected.Type, actual.Type);
Assert.Equal(expected.IsExternal, actual.IsExternal);
Assert.Equal(expected.Path, actual.Path);
Assert.Equal(expected.IsDefault, actual.IsDefault);
Assert.Equal(expected.IsForced, actual.IsForced);
Assert.Equal(expected.Language, actual.Language);
}
}
private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false)
{
return new ()
{
Index = index,
Codec = codec,
Type = MediaStreamType.Subtitle,
IsExternal = true,
Path = path,
IsDefault = isDefault,
IsForced = isForced,
Language = language
};
}
}
}

View File

@@ -0,0 +1,27 @@
using MediaBrowser.Providers.Plugins.Tmdb;
using Xunit;
namespace Jellyfin.Providers.Tests.Tmdb
{
public static class TmdbUtilsTests
{
[Theory]
[InlineData("de", "de")]
[InlineData("En", "En")]
[InlineData("de-de", "de-DE")]
[InlineData("en-US", "en-US")]
[InlineData("de-CH", "de")]
public static void NormalizeLanguage_Valid_Success(string input, string expected)
{
Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input));
}
[Theory]
[InlineData(null, null)]
[InlineData("", "")]
public static void NormalizeLanguage_Invalid_Equal(string? input, string? expected)
{
Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!));
}
}
}

View File

@@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Data
{
public class SqliteItemRepositoryTests
{
public const string VirtualMetaDataPath = "%MetadataPath%";
public const string MetaDataPath = "/meta/data/path";
private readonly IFixture _fixture;
private readonly SqliteItemRepository _sqliteItemRepository;
public SqliteItemRepositoryTests()
{
var appHost = new Mock<IServerApplicationHost>();
appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
.Returns((string x) => x.Replace(VirtualMetaDataPath, MetaDataPath, StringComparison.Ordinal));
appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
.Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_fixture.Inject(appHost);
_sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
}
public static IEnumerable<object[]> ItemImageInfoFromValueString_Valid_TestData()
{
yield return new object[]
{
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo
{
Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
Type = ImageType.Primary,
DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
};
yield return new object[]
{
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*0*0",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
}
};
yield return new object[]
{
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
}
};
yield return new object[]
{
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*600",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
}
};
yield return new object[]
{
"%MetadataPath%/library/68/68578562b96c80a7ebd530848801f645/poster.jpg*637264380567586027*Primary*600*336",
new ItemImageInfo
{
Path = "/meta/data/path/library/68/68578562b96c80a7ebd530848801f645/poster.jpg",
Type = ImageType.Primary,
DateModified = new DateTime(637264380567586027, DateTimeKind.Utc),
Width = 600,
Height = 336
}
};
}
[Theory]
[MemberData(nameof(ItemImageInfoFromValueString_Valid_TestData))]
public void ItemImageInfoFromValueString_Valid_Success(string value, ItemImageInfo expected)
{
var result = _sqliteItemRepository.ItemImageInfoFromValueString(value);
Assert.Equal(expected.Path, result.Path);
Assert.Equal(expected.Type, result.Type);
Assert.Equal(expected.DateModified, result.DateModified);
Assert.Equal(expected.Width, result.Width);
Assert.Equal(expected.Height, result.Height);
Assert.Equal(expected.BlurHash, result.BlurHash);
}
[Theory]
[InlineData("")]
[InlineData("*")]
[InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
public void ItemImageInfoFromValueString_Invalid_Null(string value)
{
Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
}
public static IEnumerable<object[]> DeserializeImages_Valid_TestData()
{
yield return new object[]
{
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo[]
{
new ItemImageInfo()
{
Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
Type = ImageType.Primary,
DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
}
};
yield return new object[]
{
"%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg*637261226720645297*Primary*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png*637261226720805297*Logo*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg*637261226721285297*Thumb*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg*637261226721685297*Backdrop*0*0",
new ItemImageInfo[]
{
new ItemImageInfo()
{
Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg",
Type = ImageType.Primary,
DateModified = new DateTime(637261226720645297, DateTimeKind.Utc),
},
new ItemImageInfo()
{
Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png",
Type = ImageType.Logo,
DateModified = new DateTime(637261226720805297, DateTimeKind.Utc),
},
new ItemImageInfo()
{
Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg",
Type = ImageType.Thumb,
DateModified = new DateTime(637261226721285297, DateTimeKind.Utc),
},
new ItemImageInfo()
{
Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg",
Type = ImageType.Backdrop,
DateModified = new DateTime(637261226721685297, DateTimeKind.Utc),
}
}
};
}
public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
{
yield return new object[]
{
string.Empty,
Array.Empty<ItemImageInfo>()
};
yield return new object[]
{
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
new ItemImageInfo[]
{
new ()
{
Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
Type = ImageType.Primary,
DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
}
};
yield return new object[]
{
"|",
Array.Empty<ItemImageInfo>()
};
}
[Theory]
[MemberData(nameof(DeserializeImages_Valid_TestData))]
public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected)
{
var result = _sqliteItemRepository.DeserializeImages(value);
Assert.Equal(expected.Length, result.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i].Path, result[i].Path);
Assert.Equal(expected[i].Type, result[i].Type);
Assert.Equal(expected[i].DateModified, result[i].DateModified);
Assert.Equal(expected[i].Width, result[i].Width);
Assert.Equal(expected[i].Height, result[i].Height);
Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
}
}
[Theory]
[MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))]
public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected)
{
var result = _sqliteItemRepository.DeserializeImages(value);
Assert.Equal(expected.Length, result.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i].Path, result[i].Path);
Assert.Equal(expected[i].Type, result[i].Type);
Assert.Equal(expected[i].DateModified, result[i].DateModified);
Assert.Equal(expected[i].Width, result[i].Width);
Assert.Equal(expected[i].Height, result[i].Height);
Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
}
}
[Theory]
[MemberData(nameof(DeserializeImages_Valid_TestData))]
public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value)
{
Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
}
public static IEnumerable<object[]> DeserializeProviderIds_Valid_TestData()
{
yield return new object[]
{
"Imdb=tt0119567",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
}
};
yield return new object[]
{
"Imdb=tt0119567|Tmdb=330|TmdbCollection=328",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
{ "Tmdb", "330" },
{ "TmdbCollection", "328" },
}
};
yield return new object[]
{
"MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970",
new Dictionary<string, string>()
{
{ "MusicBrainzAlbum", "9d363e43-f24f-4b39-bc5a-7ef305c677c7" },
{ "MusicBrainzReleaseGroup", "63eba062-847c-3b73-8b0f-6baf27bba6fa" },
{ "AudioDbArtist", "111352" },
{ "AudioDbAlbum", "2116560" },
{ "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" },
}
};
}
[Theory]
[MemberData(nameof(DeserializeProviderIds_Valid_TestData))]
public void DeserializeProviderIds_Valid_Success(string value, Dictionary<string, string> expected)
{
var result = new ProviderIdsExtensionsTestsObject();
SqliteItemRepository.DeserializeProviderIds(value, result);
Assert.Equal(expected, result.ProviderIds);
}
[Theory]
[MemberData(nameof(DeserializeProviderIds_Valid_TestData))]
public void SerializeProviderIds_Valid_Success(string expected, Dictionary<string, string> values)
{
Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values));
}
private class ProviderIdsExtensionsTestsObject : IHasProviderIds
{
public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
}
}
}

View File

@@ -1,3 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.IO;
@@ -38,5 +41,36 @@ namespace Jellyfin.Server.Implementations.Tests.IO
Assert.Equal(expectedAbsolutePath, generatedPath);
}
}
[Theory]
[InlineData("ValidFileName", "ValidFileName")]
[InlineData("AC/DC", "AC DC")]
[InlineData("Invalid\0", "Invalid ")]
[InlineData("AC/DC\0KD/A", "AC DC KD A")]
public void GetValidFilename_ReturnsValidFilename(string filename, string expectedFileName)
{
Assert.Equal(expectedFileName, _sut.GetValidFilename(filename));
}
[SkippableFact]
public void GetFileInfo_DanglingSymlink_ExistsFalse()
{
Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link");
Directory.CreateDirectory(testFileDir);
Assert.Equal(0, symlink("thispathdoesntexist", testFileName));
Assert.True(File.Exists(testFileName));
var metadata = _sut.GetFileInfo(testFileName);
Assert.False(metadata.Exists);
}
[SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")]
[DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)]
[DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)]
private static extern int symlink(string target, string linkpath);
}
}

View File

@@ -10,6 +10,8 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
<RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
</PropertyGroup>
@@ -20,18 +22,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.15.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -39,10 +41,8 @@
<ItemGroup>
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,72 @@
using System;
using Emby.Server.Implementations.Library.Resolvers.TV;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library
{
public class EpisodeResolverTest
{
[Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{
var season = new Season { Name = "Season 1" };
var parent = new Folder { Name = "extras" };
var libraryManagerMock = new Mock<ILibraryManager>();
libraryManagerMock.Setup(x => x.GetItemById(It.IsAny<Guid>())).Returns(season);
var episodeResolver = new EpisodeResolver(libraryManagerMock.Object);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = parent,
CollectionType = CollectionType.TvShows,
FileInfo = new FileSystemMetadata()
{
FullName = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv"
}
};
Assert.Null(episodeResolver.Resolve(itemResolveArgs));
}
[Fact]
public void Resolve_GivenVideoInExtrasSeriesFolder_ResolvesToEpisode()
{
var series = new Series { Name = "Extras" };
// 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<ILibraryManager>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = series,
CollectionType = CollectionType.TvShows,
FileInfo = new FileSystemMetadata()
{
FullName = "Extras/Extras S01E01.mkv"
}
};
Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
}
private class EpisodeResolverMock : EpisodeResolver
{
public EpisodeResolverMock(ILibraryManager libraryManager) : base(libraryManager)
{
}
protected override TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) => new ();
}
}
}

View File

@@ -24,5 +24,36 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
Assert.Throws<ArgumentException>(() => PathExtensions.GetAttributeValue(input, attribute));
}
[Theory]
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff/", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/jeff's band", "/home/not jeff", "/home/not jeff/consistently inconsistent.mp3")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")]
[InlineData("/o", "/o", "/s", "/s")] // regression test for #5977
public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult)
{
Assert.True(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData(null, null, null)]
[InlineData(null, "/my/path", "/another/path")]
[InlineData("/my/path", null, "/another/path")]
[InlineData("/my/path", "/another/path", null)]
[InlineData("", "", "")]
[InlineData("/my/path", "", "")]
[InlineData("", "/another/path", "")]
[InlineData("", "", "/new/subpath")]
[InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/not jeff's band", "/home/not jeff")]
public void TryReplaceSubPath_InvalidInput_ReturnsFalseAndNull(string? path, string? subPath, string? newSubPath)
{
Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
Assert.Null(result);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
@@ -15,8 +16,6 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public class HdHomerunHostTests
{
private const string TestIp = "http://192.168.1.182";
private readonly Fixture _fixture;
private readonly HdHomerunHost _hdHomerunHost;
@@ -30,7 +29,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
return Task.FromResult(new HttpResponseMessage()
{
Content = new StreamContent(File.OpenRead("Test Data/LiveTv/" + m.RequestUri?.Segments[^1]))
Content = new StreamContent(File.OpenRead(Path.Combine("Test Data/LiveTv", m.RequestUri!.Host, m.RequestUri.Segments[^1])))
});
});
@@ -50,7 +49,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
var host = new TunerHostInfo()
{
Url = TestIp
Url = "192.168.1.182"
};
var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
@@ -65,6 +64,26 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
Assert.Equal("http://192.168.1.182:80/lineup.json", modelInfo.LineupURL);
}
[Fact]
public async Task GetModelInfo_Legacy_Success()
{
var host = new TunerHostInfo()
{
Url = "10.10.10.100"
};
var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false);
Assert.Equal("HDHomeRun DUAL", modelInfo.FriendlyName);
Assert.Equal("HDHR3-US", modelInfo.ModelNumber);
Assert.Equal("hdhomerun3_atsc", modelInfo.FirmwareName);
Assert.Equal("20200225", modelInfo.FirmwareVersion);
Assert.Equal("10xxxxx5", modelInfo.DeviceID);
Assert.Null(modelInfo.DeviceAuth);
Assert.Equal(2, modelInfo.TunerCount);
Assert.Equal("http://10.10.10.100:80", modelInfo.BaseURL);
Assert.Null(modelInfo.LineupURL);
}
[Fact]
public async Task GetModelInfo_EmptyUrl_ArgumentException()
{
@@ -81,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
var host = new TunerHostInfo()
{
Url = TestIp
Url = "192.168.1.182"
};
var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false);
@@ -93,12 +112,24 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
Assert.Equal("http://192.168.1.111:5004/auto/v4.1", channels[0].URL);
}
[Fact]
public async Task GetLineup_Legacy_Success()
{
var host = new TunerHostInfo()
{
Url = "10.10.10.100"
};
// Placeholder json is invalid, just need to make sure we can reach it
await Assert.ThrowsAsync<JsonException>(() => _hdHomerunHost.GetLineup(host, CancellationToken.None));
}
[Fact]
public async Task GetLineup_ImportFavoritesOnly_Success()
{
var host = new TunerHostInfo()
{
Url = TestIp,
Url = "192.168.1.182",
ImportFavoritesOnly = true
};
@@ -114,9 +145,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
[Fact]
public async Task TryGetTunerHostInfo_Valid_Success()
{
var host = await _hdHomerunHost.TryGetTunerHostInfo(TestIp, CancellationToken.None).ConfigureAwait(false);
var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None).ConfigureAwait(false);
Assert.Equal(_hdHomerunHost.Type, host.Type);
Assert.Equal(TestIp, host.Url);
Assert.Equal("192.168.1.182", host.Url);
Assert.Equal("HDHomeRun PRIME", host.FriendlyName);
Assert.Equal("FFFFFFFF", host.DeviceId);
Assert.Equal(3, host.TunerCount);

View File

@@ -0,0 +1,326 @@
using System;
using System.Text;
using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public class HdHomerunManagerTests
{
[Fact]
public void WriteNullTerminatedString_Empty_Success()
{
ReadOnlySpan<byte> expected = stackalloc byte[]
{
1, 0
};
Span<byte> buffer = stackalloc byte[128];
int len = HdHomerunManager.WriteNullTerminatedString(buffer, string.Empty);
Assert.Equal(
Convert.ToHexString(expected),
Convert.ToHexString(buffer.Slice(0, len)));
}
[Fact]
public void WriteNullTerminatedString_Valid_Success()
{
ReadOnlySpan<byte> expected = stackalloc byte[]
{
10, (byte)'T', (byte)'h', (byte)'e', (byte)' ', (byte)'q', (byte)'u', (byte)'i', (byte)'c', (byte)'k', 0
};
Span<byte> buffer = stackalloc byte[128];
int len = HdHomerunManager.WriteNullTerminatedString(buffer, "The quick");
Assert.Equal(
Convert.ToHexString(expected),
Convert.ToHexString(buffer.Slice(0, len)));
}
[Fact]
public void WriteGetMessage_Valid_Success()
{
ReadOnlySpan<byte> expected = stackalloc byte[]
{
0, 4,
0, 12,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
0xc0, 0xc9, 0x87, 0x33
};
Span<byte> buffer = stackalloc byte[128];
int len = HdHomerunManager.WriteGetMessage(buffer, 0, "N");
Assert.Equal(
Convert.ToHexString(expected),
Convert.ToHexString(buffer.Slice(0, len)));
}
[Fact]
public void WriteSetMessage_NoLockKey_Success()
{
ReadOnlySpan<byte> expected = stackalloc byte[]
{
0, 4,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0xa9, 0x49, 0xd0, 0x68
};
Span<byte> buffer = stackalloc byte[128];
int len = HdHomerunManager.WriteSetMessage(buffer, 0, "N", "value", null);
Assert.Equal(
Convert.ToHexString(expected),
Convert.ToHexString(buffer.Slice(0, len)));
}
[Fact]
public void WriteSetMessage_LockKey_Success()
{
ReadOnlySpan<byte> expected = stackalloc byte[]
{
0, 4,
0, 26,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
21,
4, 0x00, 0x01, 0x38, 0xd5,
0x8e, 0xb6, 0x06, 0x82
};
Span<byte> buffer = stackalloc byte[128];
int len = HdHomerunManager.WriteSetMessage(buffer, 0, "N", "value", 80085);
Assert.Equal(
Convert.ToHexString(expected),
Convert.ToHexString(buffer.Slice(0, len)));
}
[Fact]
public void TryGetReturnValueOfGetSet_Valid_Success()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x7d, 0xa3, 0xa3, 0xf3
};
Assert.True(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out var value));
Assert.Equal("value", Encoding.UTF8.GetString(value));
}
[Fact]
public void TryGetReturnValueOfGetSet_InvalidCrc_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x7d, 0xa3, 0xa3, 0xf4
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_InvalidPacketType_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 4,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0xa9, 0x49, 0xd0, 0x68
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_InvalidPacket_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
0x7d, 0xa3, 0xa3
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_TooSmallMessageLength_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 19,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x25, 0x25, 0x44, 0x9a
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_TooLargeMessageLength_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 21,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0xe3, 0x20, 0x79, 0x6c
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_TooLargeNameLength_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
20, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0xe1, 0x8e, 0x9c, 0x74
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_InvalidGetSetNameTag_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
4,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0xee, 0x05, 0xe7, 0x12
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_InvalidGetSetValueTag_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
3,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x64, 0xaa, 0x66, 0xf9
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void TryGetReturnValueOfGetSet_TooLargeValueLength_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
7, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0xc9, 0xa8, 0xd4, 0x55
};
Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _));
}
[Fact]
public void VerifyReturnValueOfGetSet_Valid_True()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x7d, 0xa3, 0xa3, 0xf3
};
Assert.True(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "value"));
}
[Fact]
public void VerifyReturnValueOfGetSet_WrongValue_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 5,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x7d, 0xa3, 0xa3, 0xf3
};
Assert.False(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "none"));
}
[Fact]
public void VerifyReturnValueOfGetSet_InvalidPacket_False()
{
ReadOnlySpan<byte> packet = new byte[]
{
0, 4,
0, 20,
3,
10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0,
4,
6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0,
0x7d, 0xa3, 0xa3, 0xf3
};
Assert.False(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "value"));
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using Emby.Server.Implementations.LiveTv.EmbyTV;
using MediaBrowser.Controller.LiveTv;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public static class RecordingHelperTests
{
public static IEnumerable<object[]> GetRecordingName_Success_TestData()
{
yield return new object[]
{
"The Incredibles 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Incredibles",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsMovie = true
}
};
yield return new object[]
{
"The Incredibles (2004)",
new TimerInfo
{
Name = "The Incredibles",
IsMovie = true,
ProductionYear = 2004
}
};
yield return new object[]
{
"The Big Bang Theory 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Big Bang Theory",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsProgramSeries = true,
}
};
yield return new object[]
{
"The Big Bang Theory S12E10",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
SeasonNumber = 12,
EpisodeNumber = 10
}
};
yield return new object[]
{
"The Big Bang Theory S12E10 The VCR Illumination",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
SeasonNumber = 12,
EpisodeNumber = 10,
EpisodeTitle = "The VCR Illumination"
}
};
yield return new object[]
{
"The Big Bang Theory 2018-12-06",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6)
}
};
yield return new object[]
{
"The Big Bang Theory 2018-12-06 - The VCR Illumination",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
}
};
yield return new object[]
{
"The Big Bang Theory 2018_12_06_21_06_00 - The VCR Illumination",
new TimerInfo
{
Name = "The Big Bang Theory",
StartDate = new DateTime(2018, 12, 6, 21, 6, 0, DateTimeKind.Local),
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
}
};
}
[Theory]
[MemberData(nameof(GetRecordingName_Success_TestData))]
public static void GetRecordingName_Success(string expected, TimerInfo timerInfo)
{
Assert.Equal(expected, RecordingHelper.GetRecordingName(timerInfo));
}
}
}

View File

@@ -0,0 +1 @@
{"FriendlyName":"HDHomeRun DUAL","ModelNumber":"HDHR3-US","Legacy":1,"FirmwareName":"hdhomerun3_atsc","FirmwareVersion":"20200225","DeviceID":"10xxxxx5","TunerCount":2,"BaseURL":"http://10.10.10.100:80"}

View File

@@ -0,0 +1,684 @@
[
{
"guid": "a4df60c5-6ab4-412a-8f79-2cab93fb2bc5",
"name": "Anime",
"description": "Manage your anime in Jellyfin. This plugin supports several different metadata providers and options for organizing your collection.\n",
"overview": "Manage your anime from Jellyfin",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "10.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_10.0.0.0.zip",
"checksum": "93e969adeba1050423fc8817ed3c36f8",
"timestamp": "2020-08-17T01:41:13Z"
},
{
"version": "9.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_9.0.0.0.zip",
"checksum": "9b1cebff835813e15f414f44b40c41c8",
"timestamp": "2020-07-20T01:30:16Z"
}
]
},
{
"guid": "70b7b43b-471b-4159-b4be-56750c795499",
"name": "Auto Organize",
"description": "Automatically organize your media",
"overview": "Automatically organize your media",
"owner": "jellyfin",
"category": "General",
"versions": [
{
"version": "9.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/auto-organize/auto-organize_9.0.0.0.zip",
"checksum": "ff29ac3cbe05d208b6af94cd6d9dea39",
"timestamp": "2020-12-05T22:31:12Z"
},
{
"version": "8.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/auto-organize/auto-organize_8.0.0.0.zip",
"checksum": "460bbb45e556464a8476b18e41c097f5",
"timestamp": "2020-07-20T01:30:25Z"
}
]
},
{
"guid": "9c4e63f1-031b-4f25-988b-4f7d78a8b53e",
"name": "Bookshelf",
"description": "Supports several different metadata providers and options for organizing your collection.\n",
"overview": "Manage your books",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "5.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/bookshelf/bookshelf_5.0.0.0.zip",
"checksum": "2063fb8ab317b8d77b200fde41eb5e1e",
"timestamp": "2020-12-05T22:03:13Z"
},
{
"version": "4.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/bookshelf/bookshelf_4.0.0.0.zip",
"checksum": "fc9f76c0815d766491e5b0f30ede55ed",
"timestamp": "2020-07-20T01:30:33Z"
}
]
},
{
"guid": "cfa0f7f4-4155-4d71-849b-d6598dc4c5bb",
"name": "Email",
"description": "Send SMTP email notifications",
"overview": "Send SMTP email notifications",
"owner": "jellyfin",
"category": "Notifications",
"versions": [
{
"version": "9.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/email/email_9.0.0.0.zip",
"checksum": "cfe7afc00f3fbd6d6ab8244d7ff968ce",
"timestamp": "2020-12-05T22:20:32Z"
},
{
"version": "7.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/email/email_7.0.0.0.zip",
"checksum": "680ca511d8ad84923cb04f024fd8eb19",
"timestamp": "2020-07-20T01:30:40Z"
}
]
},
{
"guid": "170a157f-ac6c-437a-abdd-ca9c25cebd39",
"name": "Fanart",
"description": "Scrape poster images for movies, shows, and artists in your library.",
"overview": "Scrape poster images from Fanart",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/fanart/fanart_6.0.0.0.zip",
"checksum": "ee4360bfcc8722d5a3a54cfe7eef640f",
"timestamp": "2020-12-05T22:25:43Z"
},
{
"version": "5.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/fanart/fanart_5.0.0.0.zip",
"checksum": "f842f7d65d23f377761c907d40b89647",
"timestamp": "2020-07-20T01:30:48Z"
}
]
},
{
"guid": "e29621a5-fa9e-4330-982e-ef6e54c0cad2",
"name": "Gotify Notification",
"description": "You must have a Gotify server to use this plugin!\n",
"overview": "Sends notifications to your Gotify server",
"owner": "crobibero",
"category": "Notifications",
"versions": [
{
"version": "7.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/gotify-notification/gotify-notification_7.0.0.0.zip",
"checksum": "7c5ff9e8792c8cdee7e8a2aaeb6cc093",
"timestamp": "2020-07-20T01:30:56Z"
}
]
},
{
"guid": "a59b5c4b-05a8-488f-bfa8-7a63fffc7639",
"name": "IPTV",
"description": "Enable IPTV support in Jellyfin",
"overview": "Enable IPTV support in Jellyfin",
"owner": "jellyfin",
"category": "Channel",
"versions": [
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/iptv/iptv_6.0.0.0.zip",
"checksum": "9cf103bf67a4eda7c3a42d9b235f6447",
"timestamp": "2020-07-20T01:31:05Z"
}
]
},
{
"guid": "4682DD4C-A675-4F1B-8E7C-79ADF137A8F8",
"name": "ISO Mounter",
"description": "Mount your ISO files for Jellyfin.\n",
"overview": "Mount your ISO files for Jellyfin",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "1.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/iso-mounter/iso-mounter_1.0.0.0.zip",
"checksum": "847e5bc7ac34c1bf4dc5b28173170fae",
"timestamp": "2020-07-20T01:31:13Z"
}
]
},
{
"guid": "771e19d6-5385-4caf-b35c-28a0e865cf63",
"name": "Kodi Sync Queue",
"description": "This plugin will track all media changes while Kodi clients are offline to decrease sync times.",
"overview": "Sync all media changes with Kodi clients",
"owner": "jellyfin",
"category": "General",
"versions": [
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_6.0.0.0.zip",
"checksum": "787c856c0d2ad2224cdd8b3094cf0329",
"timestamp": "2020-12-05T22:10:37Z"
},
{
"version": "5.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_5.0.0.0.zip",
"checksum": "08285397aecd93ea64a4f15d38b1bd7b",
"timestamp": "2020-07-20T01:31:22Z"
}
]
},
{
"guid": "958aad66-3784-4d2a-b89a-a7b6fab6e25c",
"name": "LDAP Authentication",
"description": "Authenticate your Jellyfin users against an LDAP database, and optionally create users who do not yet exist automatically.\nAllows the administrator to customize most aspects of the LDAP authentication process, including customizable search attributes, username attribute, and a search filter for administrative users (set on user creation). The user, via the \"Manual Login\" process, can enter any valid attribute value, which will be mapped back to the specified username attribute automatically as well.\n",
"overview": "Authenticate users against an LDAP database",
"owner": "jellyfin",
"category": "Authentication",
"versions": [
{
"version": "10.0.0.0",
"changelog": "Update for 10.7 support\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_10.0.0.0.zip",
"checksum": "62e7e1cd3ffae0944c14750a3c90df4f",
"timestamp": "2020-12-05T19:48:10Z"
},
{
"version": "9.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_9.0.0.0.zip",
"checksum": "7f2f83587a65a43ebf168e4058421463",
"timestamp": "2020-07-22T15:42:57Z"
},
{
"version": "8.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_8.0.0.0.zip",
"checksum": "8af8cee62717d63577f8b1e710839415",
"timestamp": "2020-07-20T01:31:30Z"
}
]
},
{
"guid": "9574ac10-bf23-49bc-949f-924f23cfa48f",
"name": "NextPVR",
"description": "Provides access to live TV, program guide, and recordings from NextPVR.\n",
"overview": "Live TV plugin for NextPVR",
"owner": "jellyfin",
"category": "LiveTV",
"versions": [
{
"version": "5.0.0.0",
"changelog": "Updated to use NextPVR API v5, no longer compatable with API v4.\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_5.0.0.0.zip",
"checksum": "d70f694d14bf9462ba2b2ebe110068d3",
"timestamp": "2020-12-05T22:24:03Z"
},
{
"version": "4.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_4.0.0.0.zip",
"checksum": "b15949d895ac5a8c89496581db350478",
"timestamp": "2020-07-20T01:31:38Z"
}
]
},
{
"guid": "4b9ed42f-5185-48b5-9803-6ff2989014c4",
"name": "Open Subtitles",
"description": "Download subtitles from the internet to use with your media files.",
"overview": "Download subtitles for your media",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "10.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/open-subtitles/open-subtitles_10.0.0.0.zip",
"checksum": "ed99d03ec463bf15fca1256a113f57b4",
"timestamp": "2020-12-05T21:56:19Z"
},
{
"version": "9.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/open-subtitles/open-subtitles_9.0.0.0.zip",
"checksum": "16789b26497cea0509daf6b18c579340",
"timestamp": "2020-07-20T01:32:00Z"
}
]
},
{
"guid": "5c534381-91a3-43cb-907a-35aa02eb9d2c",
"name": "Playback Reporting",
"description": "Collect and show user play statistics",
"overview": "Collect and show user play statistics",
"owner": "jellyfin",
"category": "General",
"versions": [
{
"version": "9.0.0.0",
"changelog": "Add authentication to plugin endpoints\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_9.0.0.0.zip",
"checksum": "ca323b3dcb2cb86cc2e72a7a0f1eee22",
"timestamp": "2020-12-05T22:15:48Z"
},
{
"version": "8.0.0.0",
"changelog": "Add authentication to plugin endpoints\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_8.0.0.0.zip",
"checksum": "58644c505586542ef0b8b65e2f704bd1",
"timestamp": "2020-11-18T03:01:51Z"
},
{
"version": "7.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_7.0.0.0.zip",
"checksum": "6a361ef33bca97f9155856d02ff47380",
"timestamp": "2020-07-20T01:32:09Z"
}
]
},
{
"guid": "de228f12-e43e-4bd9-9fc0-2830819c3b92",
"name": "Pushbullet",
"description": "Get notifications via Pushbullet.\n",
"overview": "Pushbullet notification plugin",
"owner": "jellyfin",
"category": "Notifications",
"versions": [
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushbullet/pushbullet_6.0.0.0.zip",
"checksum": "248cf3d56644f1d909e75aaddbdfb3a6",
"timestamp": "2020-12-06T02:47:53Z"
},
{
"version": "5.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushbullet/pushbullet_5.0.0.0.zip",
"checksum": "dabbdd86328b2922a69dfa0c9e1c8343",
"timestamp": "2020-07-20T01:32:17Z"
}
]
},
{
"guid": "F240D6BE-5743-441B-87F1-A70ECAC42642",
"name": "Pushover",
"description": "Send messages to a wide range of devices through Pushover.",
"overview": "Send notifications via Pushover",
"owner": "crobibero",
"category": "Notifications",
"versions": [
{
"version": "4.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushover/pushover_4.0.0.0.zip",
"checksum": "56a0da16c7e48cc184987737b7e155dd",
"timestamp": "2020-07-20T01:32:25Z"
}
]
},
{
"guid": "d4312cd9-5c90-4f38-82e8-51da566790e8",
"name": "Reports",
"description": "Generate reports of your media library",
"overview": "Generate reports of your media library",
"owner": "jellyfin",
"category": "General",
"versions": [
{
"version": "11.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_11.0.0.0.zip",
"checksum": "d71bc6a4c008e58ee70ad44c83bfd310",
"timestamp": "2020-12-05T22:00:46Z"
},
{
"version": "10.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_10.0.0.0.zip",
"checksum": "3917e75839337475b42daf2ba0b5bd7b",
"timestamp": "2020-10-19T19:30:41Z"
},
{
"version": "9.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_9.0.0.0.zip",
"checksum": "5b5ad8d885616a21e8d1e8eecf5ea979",
"timestamp": "2020-10-16T23:52:37Z"
}
]
},
{
"guid": "1fc322a1-af2e-49a5-b2eb-a89b4240f700",
"name": "ServerWMC",
"description": "Provides access to Live TV, Program Guide and Recordings from your Windows MediaCenter Server running ServerWMC. Requires ServerWMC to be installed and running on your Windows MediaCenter machine.\n",
"overview": "Jellyfin Live TV plugin for Windows MediaCenter with ServerWMC",
"owner": "jellyfin",
"category": "LiveTV",
"versions": [
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/serverwmc/serverwmc_6.0.0.0.zip",
"checksum": "3120af0cea2c1cb8b7cf578d9b4b862c",
"timestamp": "2020-12-05T22:28:15Z"
},
{
"version": "5.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/serverwmc/serverwmc_5.0.0.0.zip",
"checksum": "dc44b039aa1b66eaf40a44fbf02d37e2",
"timestamp": "2020-07-20T01:32:42Z"
}
]
},
{
"guid": "94fb77c3-55ad-4c50-bf4e-4e5497467b79",
"name": "Slack Notifications",
"description": "Get notifications via Slack.\n",
"overview": "Get notifications via Slack",
"owner": "jellyfin",
"category": "Notifications",
"versions": [
{
"version": "7.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/slack-notifications/slack-notifications_7.0.0.0.zip",
"checksum": "1d5330a77ce7b2a9ac8e5d58088a012c",
"timestamp": "2020-12-05T22:40:02Z"
},
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/slack-notifications/slack-notifications_6.0.0.0.zip",
"checksum": "ede4cbe064542d1ecccc5823921bee4b",
"timestamp": "2020-07-20T01:32:50Z"
}
]
},
{
"guid": "bc4aad2e-d3d0-4725-a5e2-fd07949e5b42",
"name": "TMDb Box Sets",
"description": "Automatically create movie box sets based on TMDb collections",
"overview": "Automatically create movie box sets based on TMDb collections",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "7.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tmdb-box-sets/tmdb-box-sets_7.0.0.0.zip",
"checksum": "1551792e6af4d36f2cead01153c73cf0",
"timestamp": "2020-12-05T22:07:21Z"
},
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tmdb-box-sets/tmdb-box-sets_6.0.0.0.zip",
"checksum": "b92b68a922c5fcbb8f4d47b8601b01b6",
"timestamp": "2020-07-20T01:32:58Z"
}
]
},
{
"guid": "4fe3201e-d6ae-4f2e-8917-e12bda571281",
"name": "Trakt",
"description": "Record your watched media with Trakt.\n",
"overview": "Record your watched media with Trakt",
"owner": "jellyfin",
"category": "General",
"versions": [
{
"version": "11.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/trakt/trakt_11.0.0.0.zip",
"checksum": "2257ccde1e39114644a27e0966a0bf2d",
"timestamp": "2020-12-05T19:56:12Z"
},
{
"version": "10.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/trakt/trakt_10.0.0.0.zip",
"checksum": "ab67e6b59ea2e7860a6a3ff7b8452759",
"timestamp": "2020-07-20T01:33:06Z"
}
]
},
{
"guid": "3fd018e5-5e78-4e58-b280-a0c068febee0",
"name": "TVHeadend",
"description": "Manage TVHeadend from Jellyfin",
"overview": "Manage TVHeadend from Jellyfin",
"owner": "jellyfin",
"category": "LiveTV",
"versions": [
{
"version": "7.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tvheadend/tvheadend_7.0.0.0.zip",
"checksum": "1abbfce737b6962f4b1b2255dc63e932",
"timestamp": "2021-01-05T16:20:33Z"
},
{
"version": "6.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tvheadend/tvheadend_6.0.0.0.zip",
"checksum": "143c34fd70d7173b8912cc03ce4b517d",
"timestamp": "2020-07-20T01:33:15Z"
}
]
},
{
"guid": "022a3003-993f-45f1-8565-87d12af2e12a",
"name": "InfuseSync",
"description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back in to your server.",
"overview": "Blazing fast indexing for Infuse",
"owner": "Firecore LLC",
"category": "General",
"versions": [
{
"version": "1.2.4.0",
"changelog": "New Playlist support.\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.4/InfuseSync-jellyfin-1.2.4.zip",
"checksum": "7adde11b8c8404fd2923f59d98fb1a30",
"timestamp": "2020-10-12T08:00:00Z"
},
{
"version": "1.2.1.3",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.3/InfuseSync-jellyfin-1.2.3.zip",
"checksum": "d8e2c5fe736a302097bb3bac3d04b1c4",
"timestamp": "2020-09-18T12:19:00Z"
},
{
"version": "1.2.1.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.1/InfuseSync-jellyfin-1.2.1.zip",
"checksum": "1a853e926cc422f5d79d398d9ae18ee8",
"timestamp": "2020-08-21T10:48:00Z"
},
{
"version": "1.2.0.0",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.0/InfuseSync-jellyfin-1.2.0.zip",
"checksum": "2d3c7859852695a7f05adc6d3fcbc783",
"timestamp": "2020-07-20T11:51:00Z"
}
]
},
{
"guid": "8119f3c6-cfc2-4d9c-a0ba-028f1d93e526",
"name": "Cover Art Archive",
"description": "This plugin provides images from the Cover Art Archive https://musicbrainz.org/doc/Cover_Art_Archive and depends on the MusicBrainz metadata provider to know what images belong where\n",
"overview": "MusicBrainz Cover Art Archive",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "2.0.0.0",
"changelog": "changelog\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/cover-art-archive/cover-art-archive_2.0.0.0.zip",
"checksum": "bea8fa4a37b3e7ed74e22266e7597a68",
"timestamp": "2020-12-06T02:51:03Z"
},
{
"version": "1.0.0.3",
"changelog": "changelog\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/cover-art-archive/cover-art-archive_1.0.0.3.zip",
"checksum": "c502a5c54b168810614c1c40709b9598",
"timestamp": "2020-08-06T21:21:22Z"
}
]
},
{
"guid": "A4A488D0-17A3-4919-8D82-7F3DE4F6B209",
"name": "TV Maze",
"description": "Get TV metadata from TV Maze\n",
"overview": "Get TV metadata from TV Maze",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "5.0.0.0",
"changelog": "Get additional image types\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_5.0.0.0.zip",
"checksum": "509a85e40b1d1ac36eef45673deaf606",
"timestamp": "2020-12-06T02:51:56Z"
},
{
"version": "4.0.0.0",
"changelog": "Get additional image types\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_4.0.0.0.zip",
"checksum": "58ee9ab3f129151bdfff033ad889ad87",
"timestamp": "2020-11-24T14:44:37Z"
},
{
"version": "3.0.0.0",
"changelog": "Remove unused dependencies \n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_3.0.0.0.zip",
"checksum": "f3b2c70b3e136fb15c917e4420f4fdec",
"timestamp": "2020-11-09T14:32:56Z"
},
{
"version": "2.0.0.0",
"changelog": "Remove unused dependencies \n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_2.0.0.0.zip",
"checksum": "c7662ae8ae52ce8a4e8d685d55f36e80",
"timestamp": "2020-11-09T02:33:11Z"
},
{
"version": "1.0.0.0",
"changelog": "Initial release.\n",
"targetAbi": "10.6.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_1.0.0.0.zip",
"checksum": "c90eee48c12f2c07880b4b28e507fd14",
"timestamp": "2020-11-08T19:05:32Z"
}
]
},
{
"guid": "a677c0da-fac5-4cde-941a-7134223f14c8",
"name": "TheTVDB",
"description": "Get TV metadata from TheTvdb\n",
"overview": "Get TV metadata from TheTvdb",
"owner": "jellyfin",
"category": "Metadata",
"versions": [
{
"version": "2.0.0.0",
"changelog": "Remove from Jellyfin core.\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/thetvdb/thetvdb_2.0.0.0.zip",
"checksum": "e46cee334476a1b475e5c553171c4cb6",
"timestamp": "2020-12-16T20:03:28Z"
},
{
"version": "1.0.0.0",
"changelog": "Remove from Jellyfin core.\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/thetvdb/thetvdb_1.0.0.0.zip",
"checksum": "5a3dca5c0db4824d83bfd4e7e2b7bf11",
"timestamp": "2020-12-06T02:56:40Z"
}
]
}
]

View File

@@ -0,0 +1,82 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.Updates;
using MediaBrowser.Model.Updates;
using Moq;
using Moq.Protected;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Updates
{
public class InstallationManagerTests
{
private readonly Fixture _fixture;
private readonly InstallationManager _installationManager;
public InstallationManagerTests()
{
var messageHandler = new Mock<HttpMessageHandler>();
messageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(
(m, _) =>
{
return Task.FromResult(new HttpResponseMessage()
{
Content = new StreamContent(File.OpenRead("Test Data/Updates/" + m.RequestUri?.Segments[^1]))
});
});
var http = new Mock<IHttpClientFactory>();
http.Setup(x => x.CreateClient(It.IsAny<string>()))
.Returns(new HttpClient(messageHandler.Object));
_fixture = new Fixture();
_fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true
}).Inject(http);
_installationManager = _fixture.Create<InstallationManager>();
}
[Fact]
public async Task GetPackages_Valid_Success()
{
PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
false);
Assert.Equal(25, packages.Length);
}
[Fact]
public async Task FilterPackages_NameOnly_Success()
{
PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
false);
packages = _installationManager.FilterPackages(packages, "Anime").ToArray();
Assert.Single(packages);
}
[Fact]
public async Task FilterPackages_GuidOnly_Success()
{
PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
false);
packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
Assert.Single(packages);
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using Jellyfin.Server.Implementations.Users;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Users
{
public class UserManagerTests
{
[Theory]
[InlineData("this_is_valid")]
[InlineData("this is also valid")]
[InlineData("0@_-' .")]
public void ThrowIfInvalidUsername_WhenValidUsername_DoesNotThrowArgumentException(string username)
{
var ex = Record.Exception(() => UserManager.ThrowIfInvalidUsername(username));
Assert.Null(ex);
}
[Theory]
[InlineData(" ")]
[InlineData("")]
[InlineData("special characters like & $ ? are not allowed")]
public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username)
{
Assert.Throws<ArgumentException>(() => UserManager.ThrowIfInvalidUsername(username));
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
using Jellyfin.Api.Models.UserDtos;
using MediaBrowser.Common.Json;
using Xunit;
namespace Jellyfin.Server.Integration.Tests
{
public static class AuthHelper
{
public const string AuthHeaderName = "X-Emby-Authorization";
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
public static async Task<string> CompleteStartupAsync(HttpClient client)
{
var jsonOptions = JsonDefaults.Options;
var userResponse = await client.GetByteArrayAsync("/Startup/User").ConfigureAwait(false);
var user = JsonSerializer.Deserialize<StartupUserDto>(userResponse, jsonOptions);
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(
new AuthenticateUserByName()
{
Username = user!.Name,
Pw = user.Password,
},
jsonOptions));
content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false);
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
await authResponse.Content.ReadAsStreamAsync().ConfigureAwait(false),
jsonOptions).ConfigureAwait(false);
return auth!.AccessToken;
}
public static void AddAuthHeader(this HttpHeaders headers, string accessToken)
{
headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
}
private class AuthenticationResultDto
{
public string AccessToken { get; set; } = string.Empty;
public string ServerId { get; set; } = string.Empty;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using Xunit;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
public sealed class ActivityLogControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
private static string? _accessToken;
public ActivityLogControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task ActivityLog_GetEntries_Ok()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
var response = await client.GetAsync("System/ActivityLog/Entries").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
}
}
}

View File

@@ -0,0 +1,14 @@
using Jellyfin.Api;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
/// <summary>
/// Base controller for testing infrastructure.
/// Automatically ignored in generated openapi spec.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class BaseJellyfinTestController : BaseJellyfinApiController
{
}
}

View File

@@ -1,15 +1,18 @@
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MediaBrowser.Model.Branding;
using Xunit;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Integration.Tests
{
public sealed class BrandingServiceTests : IClassFixture<JellyfinApplicationFactory>
public sealed class BrandingControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
public BrandingServiceTests(JellyfinApplicationFactory factory)
public BrandingControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
@@ -24,8 +27,9 @@ namespace Jellyfin.Api.Tests
var response = await client.GetAsync("/Branding/Configuration");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
var responseBody = await response.Content.ReadAsStreamAsync();
_ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody);
}
@@ -42,8 +46,9 @@ namespace Jellyfin.Api.Tests
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType?.ToString());
Assert.True(response.IsSuccessStatusCode);
Assert.Equal("text/css", response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
}
}
}

View File

@@ -0,0 +1,86 @@
using System.IO;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Json;
using Xunit;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
public sealed class DashboardControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
public DashboardControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task GetDashboardConfigurationPage_NonExistingPage_NotFound()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetDashboardConfigurationPage_ExistingPage_CorrectPage()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType);
StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!);
Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd());
}
[Fact]
public async Task GetDashboardConfigurationPage_BrokenPage_NotFound()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetConfigurationPages_NoParams_AllConfigurationPages()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var res = await response.Content.ReadAsStreamAsync();
_ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
// TODO: check content
}
[Fact]
public async Task GetConfigurationPages_True_MainMenuConfigurationPages()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
var res = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
Assert.Empty(data);
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
/// <summary>
/// Controller for testing the encoded url.
/// </summary>
public class EncoderController : BaseJellyfinTestController
{
/// <summary>
/// Tests the url decoding.
/// </summary>
/// <param name="params">Parameters to echo back in the response.</param>
/// <returns>An <see cref="OkResult"/>.</returns>
/// <response code="200">Information retrieved.</response>
[HttpGet("UrlDecode")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ContentResult TestUrlDecoding([FromQuery] Dictionary<string, string>? @params = null)
{
return new ContentResult()
{
Content = (@params != null && @params.Count > 0)
? string.Join("&", @params.Select(x => x.Key + "=" + x.Value))
: string.Empty,
ContentType = "text/plain; charset=utf-8",
StatusCode = 200
};
}
/// <summary>
/// Tests the url decoding.
/// </summary>
/// <param name="params">Parameters to echo back in the response.</param>
/// <returns>An <see cref="OkResult"/>.</returns>
/// <response code="200">Information retrieved.</response>
[HttpGet("UrlArrayDecode")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ContentResult TestUrlArrayDecoding([FromQuery] Dictionary<string, string[]>? @params = null)
{
return new ContentResult()
{
Content = (@params != null && @params.Count > 0)
? string.Join("&", @params.Select(x => x.Key + "=" + string.Join(',', x.Value)))
: string.Empty,
ContentType = "text/plain; charset=utf-8",
StatusCode = 200
};
}
}
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
using MediaBrowser.Common.Json;
using Xunit;
using Xunit.Priority;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class StartupControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public StartupControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Fact]
[Priority(-2)]
public async Task Configuration_EditConfig_Success()
{
var client = _factory.CreateClient();
var config = new StartupConfigurationDto()
{
UICulture = "NewCulture",
MetadataCountryCode = "be",
PreferredMetadataLanguage = "nl"
};
using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(config, _jsonOptions));
postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
using var postResponse = await client.PostAsync("/Startup/Configuration", postContent).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
using var responseStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions).ConfigureAwait(false);
Assert.Equal(config.UICulture, newConfig!.UICulture);
Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode);
Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage);
}
[Fact]
[Priority(-2)]
public async Task User_DefaultUser_NameWithoutPassword()
{
var client = _factory.CreateClient();
using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false);
Assert.NotEmpty(user!.Name);
Assert.Null(user.Password);
}
[Fact]
[Priority(-1)]
public async Task User_EditUser_Success()
{
var client = _factory.CreateClient();
var user = new StartupUserDto()
{
Name = "NewName",
Password = "NewPassword"
};
using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions));
postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
var postResponse = await client.PostAsync("/Startup/User", postContent).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
var contentStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false);
Assert.Equal(user.Name, newUser!.Name);
Assert.NotEmpty(newUser.Password);
Assert.NotEqual(user.Password, newUser.Password);
}
[Fact]
[Priority(0)]
public async Task CompleteWizard_Success()
{
var client = _factory.CreateClient();
var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
[Fact]
[Priority(1)]
public async Task GetFirstUser_CompleteWizard_Unauthorized()
{
var client = _factory.CreateClient();
using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.UserDtos;
using MediaBrowser.Common.Json;
using MediaBrowser.Model.Dto;
using Xunit;
using Xunit.Priority;
namespace Jellyfin.Server.Integration.Tests.Controllers
{
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class UserControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private const string TestUsername = "testUser01";
private readonly JellyfinApplicationFactory _factory;
private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
private static string? _accessToken;
private static Guid _testUserId = Guid.Empty;
public UserControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
private Task<HttpResponseMessage> CreateUserByName(HttpClient httpClient, CreateUserByName request)
{
using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
return httpClient.PostAsync("Users/New", postContent);
}
private Task<HttpResponseMessage> UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request)
{
using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
return httpClient.PostAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", postContent);
}
[Fact]
[Priority(-1)]
public async Task GetPublicUsers_Valid_Success()
{
var client = _factory.CreateClient();
using var response = await client.GetAsync("Users/Public").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
// User are hidden by default
Assert.Empty(users);
}
[Fact]
[Priority(-1)]
public async Task GetUsers_Valid_Success()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
using var response = await client.GetAsync("Users").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
Assert.Single(users);
Assert.False(users![0].HasConfiguredPassword);
}
[Fact]
[Priority(0)]
public async Task New_Valid_Success()
{
var client = _factory.CreateClient();
// access token can't be null here as the previous test populated it
client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
var createRequest = new CreateUserByName()
{
Name = TestUsername
};
using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = await JsonSerializer.DeserializeAsync<UserDto>(
await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
Assert.Equal(TestUsername, user!.Name);
Assert.False(user.HasPassword);
Assert.False(user.HasConfiguredPassword);
_testUserId = user.Id;
Console.WriteLine(user.Id.ToString("N", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("‼️")]
[Priority(0)]
public async Task New_Invalid_Fail(string? username)
{
var client = _factory.CreateClient();
// access token can't be null here as the previous test populated it
client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
var createRequest = new CreateUserByName()
{
Name = username
};
using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
[Priority(1)]
public async Task UpdateUserPassword_Valid_Success()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
var createRequest = new UpdateUserPassword()
{
NewPw = "4randomPa$$word"
};
using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
var user = users!.First(x => x.Id == _testUserId);
Assert.True(user.HasPassword);
Assert.True(user.HasConfiguredPassword);
}
[Fact]
[Priority(2)]
public async Task UpdateUserPassword_Empty_RemoveSetPassword()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
var createRequest = new UpdateUserPassword()
{
CurrentPw = "4randomPa$$word",
};
using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false);
var user = users!.First(x => x.Id == _testUserId);
Assert.False(user.HasPassword);
Assert.False(user.HasConfiguredPassword);
}
}
}

View File

@@ -0,0 +1,48 @@
using System.Net;
using System.Threading.Tasks;
using Xunit;
namespace Jellyfin.Server.Integration.Tests
{
/// <summary>
/// Defines the test for encoded querystrings in the url.
/// </summary>
public class EncodedQueryStringTest : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
public EncodedQueryStringTest(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Theory]
[InlineData("a=1&b=2&c=3", "a=1&b=2&c=3")] // won't be processed as there is more than 1.
[InlineData("a=1", "a=1")] // won't be processed as it has a value
[InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
[InlineData("a=b&a=c", "a=b")]
[InlineData("a%3Db%26a%3Dc", "a=b")]
public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
{
var client = _factory.CreateClient();
var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Equal(unencodedUrl, reply);
}
[Theory]
[InlineData("a=b&a=c", "a=b,c")]
[InlineData("a%3Db%26a%3Dc", "a=b,c")]
public async Task Ensure_Array_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
{
var client = _factory.CreateClient();
var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Equal(unencodedUrl, reply);
}
}
}

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<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="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<ItemGroup>
<!-- Don't run tests in parallel -->
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestPage.html" />
</ItemGroup>
</Project>

View File

@@ -1,19 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
using Jellyfin.Server;
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 Serilog;
using Serilog.Extensions.Logging;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Integration.Tests
{
/// <summary>
/// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests.
@@ -21,12 +22,12 @@ namespace Jellyfin.Api.Tests
public class JellyfinApplicationFactory : WebApplicationFactory<Startup>
{
private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
private static readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>();
private readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>();
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinApplicationFactory"/> class.
/// Initializes static members of the <see cref="JellyfinApplicationFactory"/> class.
/// </summary>
public JellyfinApplicationFactory()
static JellyfinApplicationFactory()
{
// Perform static initialization that only needs to happen once per test-run
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
@@ -73,10 +74,11 @@ namespace Jellyfin.Api.Tests
_disposableComponents.Add(loggerFactory);
// Create the app host and initialize it
var appHost = new CoreAppHost(
var appHost = new TestAppHost(
appPaths,
loggerFactory,
commandLineOpts,
new ConfigurationBuilder().Build(),
new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
_disposableComponents.Add(appHost);
@@ -93,10 +95,10 @@ namespace Jellyfin.Api.Tests
var testServer = base.CreateServer(builder);
// Finish initializing the app host
var appHost = (CoreAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
return testServer;
}

View File

@@ -1,12 +1,10 @@
using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using MediaBrowser.Model.Branding;
using Xunit;
using Xunit.Abstractions;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Integration.Tests
{
public sealed class OpenApiSpecTests : IClassFixture<JellyfinApplicationFactory>
{

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Integration.Tests
{
/// <summary>
/// Implementation of the abstract <see cref="ApplicationHost" /> class.
/// </summary>
public class TestAppHost : CoreAppHost
{
/// <summary>
/// Initializes a new instance of the <see cref="TestAppHost" /> class.
/// </summary>
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public TestAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startup,
IFileSystem fileSystem,
IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
startup,
fileSystem,
collection)
{
}
/// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
foreach (var a in base.GetAssembliesWithPartsInternal())
{
yield return a;
}
yield return typeof(TestPlugin).Assembly;
}
}
}

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>TestPlugin</title>
</head>
<body>
<h1>This is a Test Page.</h1>
</body>
</html>

View File

@@ -0,0 +1,43 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Server.Integration.Tests
{
public class TestPlugin : BasePlugin<BasePluginConfiguration>, IHasWebPages
{
public TestPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
public static TestPlugin? Instance { get; private set; }
public override Guid Id => new Guid("2d350a13-0bf7-4b61-859c-d5e601b5facf");
public override string Name => nameof(TestPlugin);
public override string Description => "Server test Plugin.";
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
{
Name = Name,
EmbeddedResourcePath = GetType().Namespace + ".TestPage.html"
};
yield return new PluginPageInfo
{
Name = "BrokenPage",
EmbeddedResourcePath = GetType().Namespace + ".foobar"
};
}
}
}

View File

@@ -0,0 +1,27 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Server.Integration.Tests
{
public class TestPluginWithoutPages : BasePlugin<BasePluginConfiguration>
{
public TestPluginWithoutPages(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
public static TestPluginWithoutPages? Instance { get; private set; }
public override Guid Id => new Guid("ae95cbe6-bd3d-4d73-8596-490db334611e");
public override string Name => nameof(TestPluginWithoutPages);
public override string Description => "Server test Plugin without web pages.";
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Jellyfin.Server.Integration.Tests
{
public sealed class WebSocketTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
public WebSocketTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task WebSocket_Unauthenticated_ThrowsInvalidOperationException()
{
var server = _factory.Server;
var client = server.CreateWebSocketClient();
await Assert.ThrowsAsync<InvalidOperationException>(
() => client.ConnectAsync(
new UriBuilder(server.BaseAddress)
{
Scheme = "ws",
Path = "websocket"
}.Uri, CancellationToken.None));
}
}
}

View File

@@ -0,0 +1,4 @@
{
"parallelizeAssembly": false,
"parallelizeTestCollections": false
}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<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="5.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
<PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,3 @@
using System;
using System.Globalization;
using System.Text;
using Jellyfin.Networking.Configuration;
@@ -10,7 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Tests
{
public class ParseNetworkTests
{
@@ -37,28 +36,28 @@ namespace Jellyfin.Api.Tests
EnableIPV6 = ip6
};
var result = match + ',';
var result = match + ",";
ForwardedHeadersOptions options = new ForwardedHeadersOptions();
// Need this here as ::1 and 127.0.0.1 are in them by default.
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(","), options);
ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(','), options);
var sb = new StringBuilder();
foreach (var item in options.KnownProxies)
{
sb.Append(item);
sb.Append(',');
sb.Append(item)
.Append(',');
}
foreach (var item in options.KnownNetworks)
{
sb.Append(item.Prefix);
sb.Append('/');
sb.Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture));
sb.Append(',');
sb.Append(item.Prefix)
.Append('/')
.Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture))
.Append(',');
}
Assert.Equal(sb.ToString(), result);

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Server.Middleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Jellyfin.Server.Tests
{
public static class UrlDecodeQueryFeatureTests
{
[Theory]
[InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
[InlineData("random+test", "random test")] // encoded
[InlineData("random%20test", "random test")] // encoded
[InlineData("++", " ")] // encoded
public static void EmptyValueTest(string query, string key)
{
var dict = new Dictionary<string, StringValues>
{
{ query, StringValues.Empty }
};
var test = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict)));
Assert.Single(test.Query);
var (k, v) = test.Query.First();
Assert.Equal(key, k);
Assert.Empty(v);
}
}
}

View File

@@ -5,6 +5,8 @@
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -14,16 +16,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
@@ -34,8 +35,4 @@
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,65 @@
using System.Linq;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.System;
using MediaBrowser.XbmcMetadata.Savers;
using Xunit;
namespace Jellyfin.XbmcMetadata.Tests.Location
{
public class MovieNfoLocationTests
{
[Fact]
public static void Movie_MixedFolder_Success()
{
var movie = new Movie() { Path = "/media/movies/Avengers Endgame.mp4", IsInMixedFolder = true };
var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
Assert.Single(paths);
Assert.Contains("/media/movies/Avengers Endgame.nfo", paths);
}
[Fact]
public static void Movie_SeparateFolder_Success()
{
var movie = new Movie() { Path = "/media/movies/Avengers Endgame/Avengers Endgame.mp4" };
var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
var path2 = "/media/movies/Avengers Endgame/movie.nfo";
// uses ContainingFolderPath which uses Operating system specific paths
if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
{
movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\');
path2 = path2.Replace('/', '\\');
}
var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
Assert.Equal(2, paths.Length);
Assert.Contains(path1, paths);
Assert.Contains(path2, paths);
}
[Fact]
public void Movie_DVD_Success()
{
var movie = new Movie() { Path = "/media/movies/Avengers Endgame", VideoType = VideoType.Dvd };
var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
var path2 = "/media/movies/Avengers Endgame/VIDEO_TS/VIDEO_TS.nfo";
// uses ContainingFolderPath which uses Operating system specific paths
if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
{
movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\');
path2 = path2.Replace('/', '\\');
}
var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
Assert.Equal(2, paths.Length);
Assert.Contains(path1, paths);
Assert.Contains(path2, paths);
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -13,8 +14,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
#pragma warning disable CA5369
namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
public class EpisodeNfoProviderTests
@@ -34,11 +33,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
_parser = new EpisodeNfoParser(new NullLogger<EpisodeNfoParser>(), config.Object, providerManager.Object);
var user = new Mock<IUserManager>();
var userData = new Mock<IUserDataManager>();
var directoryService = new Mock<IDirectoryService>();
_parser = new EpisodeNfoParser(
new NullLogger<EpisodeNfoParser>(),
config.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]
public void Fetch_Valid_Succes()
public void Fetch_Valid_Success()
{
var result = new MetadataResult<Episode>()
{
@@ -62,6 +71,10 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(2017, item.ProductionYear);
Assert.Single(item.Studios);
Assert.Contains("Starz", item.Studios);
Assert.Equal(1, item.IndexNumberEnd);
Assert.Equal(2, item.AirsAfterSeasonNumber);
Assert.Equal(3, item.AirsBeforeSeasonNumber);
Assert.Equal(1, item.AirsBeforeEpisodeNumber);
Assert.Equal("tt5017734", item.ProviderIds[MetadataProvider.Imdb.ToString()]);
Assert.Equal("1276153", item.ProviderIds[MetadataProvider.Tmdb.ToString()]);
@@ -89,6 +102,26 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(new DateTime(2017, 10, 7, 14, 25, 47), item.DateCreated);
}
[Fact]
public void Fetch_Valid_MultiEpisode_Success()
{
var result = new MetadataResult<Episode>()
{
Item = new Episode()
};
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
var item = result.Item;
Assert.Equal("Rising (1)", item.Name);
Assert.Equal(1, item.IndexNumber);
Assert.Equal(2, item.IndexNumberEnd);
Assert.Equal(1, item.ParentIndexNumber);
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
Assert.Equal(2004, item.ProductionYear);
}
[Fact]
public void Fetch_WithNullItem_ThrowsArgumentException()
{

View File

@@ -1,12 +1,17 @@
using System;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.System;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
@@ -18,9 +23,14 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
public class MovieNfoParserTests
{
private readonly MovieNfoParser _parser;
private readonly IUserDataManager _userDataManager;
private readonly User _testUser;
private readonly FileSystemMetadata _localImageFileMetadata;
public MovieNfoParserTests()
{
_testUser = new User("Test User", "Auth provider", "Reset provider");
var providerManager = new Mock<IProviderManager>();
var tmdbExternalId = new TmdbMovieExternalId();
@@ -29,22 +39,53 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
.Returns(new[] { externalIdInfo });
var config = new Mock<IConfigurationManager>();
config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(new XbmcMetadataOptions());
_parser = new MovieNfoParser(new NullLogger<MovieNfoParser>(), config.Object, providerManager.Object);
var nfoConfig = new XbmcMetadataOptions()
{
UserId = "F38E6443-090B-4F7A-BD12-9CFF5020F7BC"
};
var configManager = new Mock<IConfigurationManager>();
configManager.Setup(x => x.GetConfiguration(It.IsAny<string>()))
.Returns(nfoConfig);
var user = new Mock<IUserManager>();
user.Setup(x => x.GetUserById(It.IsAny<Guid>()))
.Returns(_testUser);
var userData = new Mock<IUserDataManager>();
userData.Setup(x => x.GetUserData(_testUser, It.IsAny<BaseItem>()))
.Returns(new UserItemData());
var directoryService = new Mock<IDirectoryService>();
_localImageFileMetadata = new FileSystemMetadata()
{
Exists = true,
FullName = MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows ?
"C:\\media\\movies\\Justice League (2017).jpg"
: "/media/movies/Justice League (2017).jpg"
};
directoryService.Setup(x => x.GetFile(_localImageFileMetadata.FullName))
.Returns(_localImageFileMetadata);
_userDataManager = userData.Object;
_parser = new MovieNfoParser(
new NullLogger<MovieNfoParser>(),
configManager.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]
public void Fetch_Valid_Succes()
public void Fetch_Valid_Success()
{
var result = new MetadataResult<Video>()
{
Item = new Video()
Item = new Movie()
};
_parser.Fetch(result, "Test Data/Justice League.nfo", CancellationToken.None);
var item = result.Item;
var item = (Movie)result.Item;
Assert.Equal("Justice League", item.OriginalTitle);
Assert.Equal("Justice for all.", item.Tagline);
@@ -58,22 +99,31 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Contains("Sci-Fi", item.Genres);
Assert.Equal(new DateTime(2017, 11, 15), item.PremiereDate);
Assert.Equal(new DateTime(2017, 11, 16), item.EndDate);
Assert.Single(item.Studios);
Assert.Contains("DC Comics", item.Studios);
Assert.Equal("1.777778", item.AspectRatio);
Assert.Equal(Video3DFormat.HalfSideBySide, item.Video3DFormat);
Assert.Equal(1920, item.Width);
Assert.Equal(1080, item.Height);
Assert.Equal(new TimeSpan(0, 0, 6268).Ticks, item.RunTimeTicks);
Assert.True(item.HasSubtitles);
Assert.Equal(7.6f, item.CriticRating);
Assert.Equal("8.7", item.CustomRating);
Assert.Equal("en", item.PreferredMetadataLanguage);
Assert.Equal("us", item.PreferredMetadataCountryCode);
Assert.Single(item.RemoteTrailers);
Assert.Equal("https://www.youtube.com/watch?v=dQw4w9WgXcQ", item.RemoteTrailers[0].Url);
Assert.Equal(19, result.People.Count);
Assert.Equal(20, result.People.Count);
var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray();
Assert.Equal(2, writers.Length);
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();
Assert.Single(directors);
@@ -94,6 +144,82 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal("Test Lyricist", lyricist!.Name);
Assert.Equal(new DateTime(2019, 8, 6, 9, 1, 18), item.DateCreated);
// userData
var userData = _userDataManager.GetUserData(_testUser, item);
Assert.Equal(2, userData.PlayCount);
Assert.True(userData.Played);
Assert.Equal(new DateTime(2021, 02, 11, 07, 47, 23), userData.LastPlayedDate);
// Movie set
Assert.Equal("702342", item.ProviderIds[MetadataProvider.TmdbCollection.ToString()]);
Assert.Equal("Justice League Collection", item.CollectionName);
// Images
Assert.Equal(7, result.RemoteImages.Count);
var posters = result.RemoteImages.Where(x => x.type == ImageType.Primary).ToList();
Assert.Single(posters);
Assert.Equal("http://image.tmdb.org/t/p/original/9rtrRGeRnL0JKtu9IMBWsmlmmZz.jpg", posters[0].url);
var logos = result.RemoteImages.Where(x => x.type == ImageType.Logo).ToList();
Assert.Single(logos);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png", logos[0].url);
var banners = result.RemoteImages.Where(x => x.type == ImageType.Banner).ToList();
Assert.Single(banners);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-586017e95adbd.jpg", banners[0].url);
var thumbs = result.RemoteImages.Where(x => x.type == ImageType.Thumb).ToList();
Assert.Single(thumbs);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585fb155c3743.jpg", thumbs[0].url);
var art = result.RemoteImages.Where(x => x.type == ImageType.Art).ToList();
Assert.Single(art);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/hdmovieclearart/justice-league-5865c23193041.png", art[0].url);
var discArt = result.RemoteImages.Where(x => x.type == ImageType.Disc).ToList();
Assert.Single(discArt);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a3af26360617.png", discArt[0].url);
var backdrop = result.RemoteImages.Where(x => x.type == ImageType.Backdrop).ToList();
Assert.Single(backdrop);
Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg", backdrop[0].url);
// Local Image - contains only one item depending on operating system
Assert.Single(result.Images);
Assert.Equal(_localImageFileMetadata.Name, result.Images[0].FileInfo.Name);
}
[Theory]
[InlineData("Test Data/Tmdb.nfo", "Tmdb", "30287")]
[InlineData("Test Data/Imdb.nfo", "Imdb", "tt0944947")]
public void Parse_UrlFile_Success(string path, string provider, string id)
{
var result = new MetadataResult<Video>()
{
Item = new Movie()
};
_parser.Fetch(result, path, CancellationToken.None);
var item = (Movie)result.Item;
Assert.Equal(id, item.ProviderIds[provider]);
}
[Fact]
public void Parse_RadarrUrlFile_Success()
{
var result = new MetadataResult<Video>()
{
Item = new Movie()
};
_parser.Fetch(result, "Test Data/Radarr.nfo", CancellationToken.None);
var item = (Movie)result.Item;
Assert.Equal("583689", item.ProviderIds[MetadataProvider.Tmdb.ToString()]);
Assert.Equal("tt4154796", item.ProviderIds[MetadataProvider.Imdb.ToString()]);
}
[Fact]
@@ -109,7 +235,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
var result = new MetadataResult<Video>()
{
Item = new Video()
Item = new Movie()
};
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));

Some files were not shown because too many files have changed in this diff Show More