Merge remote-tracking branch 'upstream/master' into UrlDecoding

This commit is contained in:
BaronGreenback
2021-05-08 12:22:09 +01:00
739 changed files with 10333 additions and 5367 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

@@ -10,15 +10,17 @@
<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.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
@@ -27,22 +29,14 @@
<!-- 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" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="TestPage.html" />
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
<ProjectReference Include="../../Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj" />
</ItemGroup>
</Project>

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

@@ -10,10 +10,12 @@
<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.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
@@ -21,7 +23,6 @@
<!-- 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 +33,4 @@
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

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,39 @@
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

@@ -10,10 +10,13 @@
<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.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<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.3" />
@@ -21,7 +24,6 @@
<!-- 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,10 +5,13 @@
<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.9.1" />
<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.3" />
@@ -16,7 +19,6 @@
<!-- 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,7 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
@@ -27,7 +29,6 @@
<!-- 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,56 @@
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/some_matadata.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/some_matadata.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);
}
}
}

View File

@@ -13,38 +13,11 @@ namespace Jellyfin.MediaEncoding.Subtitles.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 readonly SsaParser _parser = new SsaParser(new NullLogger<AssParser>());
[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
// TODO: Fix upstream
// [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
// TODO: Fix upstream
// [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)
public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
{
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
{

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,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

@@ -9,6 +9,53 @@ namespace Jellyfin.Model.Tests.Entities
{
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()
{
@@ -30,7 +77,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void GetProviderId_NullProvider_Null()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
@@ -47,7 +94,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void TryGetProviderId_NullProvider_False()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
@@ -74,6 +121,16 @@ namespace Jellyfin.Model.Tests.Entities
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()
{
@@ -108,7 +165,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void SetProviderId_NullProvider_Success()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
@@ -120,7 +177,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void SetProviderId_NullProviderAndEmptyName_Success()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};

View File

@@ -1,4 +1,3 @@
using System;
using MediaBrowser.Model.Extensions;
using Xunit;

View File

@@ -5,10 +5,12 @@
<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.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
@@ -16,7 +18,6 @@
<!-- 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 +27,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,12 +8,14 @@
<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.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
@@ -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

@@ -7,18 +7,13 @@ namespace Jellyfin.Naming.Tests.Video
{
public sealed class CleanStringTests
{
private readonly NamingOptions _namingOptions = new NamingOptions();
private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
[Theory]
[InlineData("Super movie 480p.mp4", "Super movie")]
[InlineData("Super movie 480p 2001.mp4", "Super movie")]
[InlineData("Super movie [480p].mp4", "Super movie")]
[InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")]
[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, 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, 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;

View File

@@ -295,12 +295,9 @@ namespace Jellyfin.Naming.Tests.Video
FullName = i
}).ToList()).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]
@@ -368,6 +365,44 @@ namespace Jellyfin.Naming.Tests.Video
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()).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()).ToList();
Assert.Equal(2, result.Count);
}
[Fact]
public void TestEmptyList()
{

View File

@@ -11,7 +11,7 @@ namespace Jellyfin.Naming.Tests.Video
{
private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
public static IEnumerable<object[]> GetResolveFileTestData()
public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
{
yield return new object[]
{
@@ -156,7 +156,7 @@ 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);

View File

@@ -8,12 +8,14 @@
<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.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
@@ -22,18 +24,18 @@
<!-- 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,19 +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
{
private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
internal static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
{
var configManager = new Mock<IConfigurationManager>
{
@@ -34,10 +34,12 @@ namespace Jellyfin.Networking.Tests
[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]")]
// All interfaces excluded.
[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", "[]")]
// 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]")]
[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()
@@ -54,36 +56,10 @@ 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>
/// <param name="address">IP Address.</param>
[Theory]
[InlineData("127.0.0.1")]
[InlineData("127.0.0.1:123")]
@@ -107,7 +83,7 @@ namespace Jellyfin.Networking.Tests
/// <summary>
/// Checks IP address formats.
/// </summary>
/// <param name="address"></param>
/// <param name="address">IP Address.</param>
[Theory]
[InlineData("127.0.0.1")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
@@ -124,7 +100,6 @@ namespace Jellyfin.Networking.Tests
Assert.True(IPNetAddress.TryParse(address, out _));
}
/// <summary>
/// All should be invalid address strings.
/// </summary>
@@ -141,7 +116,6 @@ namespace Jellyfin.Networking.Tests
Assert.False(IPHost.TryParse(address, out _));
}
/// <summary>
/// Test collection parsing.
/// </summary>
@@ -152,19 +126,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(
"",
"[]",
"[]",
"[]",
@@ -177,7 +154,8 @@ namespace Jellyfin.Networking.Tests
"[10.10.10.10/32]",
"[10.10.10.10/32]",
"[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",
[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 +172,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 +230,6 @@ namespace Jellyfin.Networking.Tests
throw new ArgumentNullException(nameof(result));
}
var conf = new NetworkConfiguration()
{
EnableIPV6 = true,
@@ -261,10 +238,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]
@@ -371,14 +348,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 +384,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 +419,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 +437,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")]
@@ -479,7 +461,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 +496,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,250 @@
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),
}
}
};
}
[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_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.9.1" />
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<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="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" />
@@ -42,8 +44,4 @@
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.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

@@ -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,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,57 @@
using System.Collections.Generic;
using System.IO;
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()
{
IList<PackageInfo> packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
false);
Assert.Equal(25, packages.Count);
}
}
}

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

@@ -1,3 +1,4 @@
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
@@ -5,7 +6,7 @@ using System.Threading.Tasks;
using MediaBrowser.Model.Branding;
using Xunit;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Integration.Tests
{
public sealed class BrandingControllerTests : IClassFixture<JellyfinApplicationFactory>
{
@@ -26,7 +27,7 @@ namespace Jellyfin.Api.Tests
var response = await client.GetAsync("/Branding/Configuration");
// Assert
Assert.True(response.IsSuccessStatusCode);
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();

View File

@@ -8,12 +8,12 @@ using Jellyfin.Api.Models;
using MediaBrowser.Common.Json;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers
namespace Jellyfin.Server.Integration.Tests.Controllers
{
public sealed class DashboardControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.GetOptions();
private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
public DashboardControllerTests(JellyfinApplicationFactory factory)
{
@@ -37,9 +37,9 @@ namespace Jellyfin.Api.Tests.Controllers
var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false);
Assert.True(response.IsSuccessStatusCode);
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.Api.Tests.TestPage.html")!);
StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!);
Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd());
}
@@ -60,7 +60,7 @@ namespace Jellyfin.Api.Tests.Controllers
var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false);
Assert.True(response.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var res = await response.Content.ReadAsStreamAsync();
_ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions);
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Tests.Controllers
var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false);
Assert.True(response.IsSuccessStatusCode);
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);

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,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.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<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();
@@ -77,6 +78,7 @@ namespace Jellyfin.Api.Tests
appPaths,
loggerFactory,
commandLineOpts,
new ConfigurationBuilder().Build(),
new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
_disposableComponents.Add(appHost);
@@ -96,7 +98,7 @@ namespace Jellyfin.Api.Tests
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

@@ -1,13 +1,13 @@
using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using Jellyfin.Server;
using MediaBrowser.Controller;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Integration.Tests
{
/// <summary>
/// Implementation of the abstract <see cref="ApplicationHost" /> class.
@@ -20,18 +20,21 @@ namespace Jellyfin.Api.Tests
/// <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)
{

View File

@@ -7,7 +7,7 @@ using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Api.Tests
namespace Jellyfin.Server.Integration.Tests
{
public class TestPlugin : BasePlugin<BasePluginConfiguration>, IHasWebPages
{

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.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<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
{

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,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
@@ -23,7 +25,6 @@
<!-- 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

@@ -37,8 +37,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
.Returns(new XbmcMetadataOptions());
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);
_parser = new EpisodeNfoParser(
new NullLogger<EpisodeNfoParser>(),
config.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]

View File

@@ -9,7 +9,9 @@ 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;
@@ -23,6 +25,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
private readonly MovieNfoParser _parser;
private readonly IUserDataManager _userDataManager;
private readonly User _testUser;
private readonly FileSystemMetadata _localImageFileMetadata;
public MovieNfoParserTests()
{
@@ -52,8 +55,25 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
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);
_parser = new MovieNfoParser(
new NullLogger<MovieNfoParser>(),
configManager.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]
@@ -134,6 +154,41 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
// 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]
@@ -152,6 +207,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
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]
public void Fetch_WithNullItem_ThrowsArgumentException()
{

View File

@@ -1,7 +1,6 @@
#pragma warning disable CA5369
using System;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -11,7 +10,6 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Music;
using MediaBrowser.Providers.Plugins.MusicBrainz;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
@@ -38,8 +36,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
.Returns(new XbmcMetadataOptions());
var user = new Mock<IUserManager>();
var userData = new Mock<IUserDataManager>();
var directoryService = new Mock<IDirectoryService>();
_parser = new BaseNfoParser<MusicAlbum>(new NullLogger<BaseNfoParser<MusicAlbum>>(), config.Object, providerManager.Object, user.Object, userData.Object);
_parser = new BaseNfoParser<MusicAlbum>(
new NullLogger<BaseNfoParser<MusicAlbum>>(),
config.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]

View File

@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -35,8 +34,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
.Returns(new XbmcMetadataOptions());
var user = new Mock<IUserManager>();
var userData = new Mock<IUserDataManager>();
var directoryService = new Mock<IDirectoryService>();
_parser = new BaseNfoParser<MusicArtist>(new NullLogger<BaseNfoParser<MusicArtist>>(), config.Object, providerManager.Object, user.Object, userData.Object);
_parser = new BaseNfoParser<MusicArtist>(
new NullLogger<BaseNfoParser<MusicArtist>>(),
config.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]

View File

@@ -30,8 +30,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var user = new Mock<IUserManager>();
var userData = new Mock<IUserDataManager>();
var directoryService = new Mock<IDirectoryService>();
_parser = new MovieNfoParser(new NullLogger<BaseNfoParser<MusicVideo>>(), config.Object, providerManager.Object, user.Object, userData.Object);
_parser = new MovieNfoParser(
new NullLogger<BaseNfoParser<MusicVideo>>(),
config.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]

View File

@@ -31,8 +31,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
.Returns(new XbmcMetadataOptions());
var user = new Mock<IUserManager>();
var userData = new Mock<IUserDataManager>();
var directoryService = new Mock<IDirectoryService>();
_parser = new SeasonNfoParser(new NullLogger<SeasonNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object);
_parser = new SeasonNfoParser(
new NullLogger<SeasonNfoParser>(),
config.Object,
providerManager.Object,
user.Object,
userData.Object,
directoryService.Object);
}
[Fact]

View File

@@ -29,8 +29,9 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
.Returns(new XbmcMetadataOptions());
var user = new Mock<IUserManager>();
var userData = new Mock<IUserDataManager>();
var directoryService = new Mock<IDirectoryService>();
_parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object);
_parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object, directoryService.Object);
}
[Fact]

View File

@@ -59,6 +59,8 @@
<thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg">http://image.tmdb.org/t/p/original/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg</thumb>
<thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/paLcue01KpfQftorfjKqqD4qvlL.jpg">http://image.tmdb.org/t/p/original/paLcue01KpfQftorfjKqqD4qvlL.jpg</thumb>
<thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg">http://image.tmdb.org/t/p/original/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg</thumb>
<thumb aspect="poster">C:\media\movies\Justice League (2017).jpg</thumb>
<thumb aspect="poster">/media/movies/Justice League (2017).jpg</thumb>
<thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png</thumb>
<thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png</thumb>
<thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57b476a831d74.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57b476a831d74.png</thumb>
@@ -80,8 +82,8 @@
<thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a0b913c233be.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a0b913c233be.png</thumb>
<thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png</thumb>
<thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-59dc595362ef1.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-59dc595362ef1.png</thumb>
<thumb aspect="fanart">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb>
<fanart>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg</thumb>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg</thumb>
<thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg</thumb>
@@ -91,7 +93,8 @@
</fanart>
<mpaa>Australia:M</mpaa>
<id>tt0974015</id>
<uniqueid type="imdb" default="true">tt0974015</uniqueid>
<uniqueid type="imdb">tt0974015</uniqueid>
<uniqueid type="tmdb">141052</uniqueid>
<genre>Action</genre>
<genre>Adventure</genre>
<genre>Fantasy</genre>

View File

@@ -0,0 +1,2 @@
https://www.themoviedb.org/movie/583689
https://www.imdb.com/title/tt4154796