repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
@@ -0,0 +1,102 @@
using System;
using Emby.Server.Implementations.Cryptography;
using MediaBrowser.Model.Cryptography;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Cryptography;
public class CryptographyProviderTests
{
private readonly CryptographyProvider _sut = new();
[Fact]
public void CreatePasswordHash_WithPassword_ReturnsHashWithIterations()
{
var hash = _sut.CreatePasswordHash("testpassword");
Assert.Equal("PBKDF2-SHA512", hash.Id);
Assert.True(hash.Parameters.ContainsKey("iterations"));
Assert.NotEmpty(hash.Salt.ToArray());
Assert.NotEmpty(hash.Hash.ToArray());
}
[Fact]
public void Verify_WithValidPassword_ReturnsTrue()
{
const string password = "testpassword";
var hash = _sut.CreatePasswordHash(password);
Assert.True(_sut.Verify(hash, password));
}
[Fact]
public void Verify_WithWrongPassword_ReturnsFalse()
{
var hash = _sut.CreatePasswordHash("correctpassword");
Assert.False(_sut.Verify(hash, "wrongpassword"));
}
[Fact]
public void Verify_PBKDF2_MissingIterations_ThrowsFormatException()
{
var hash = PasswordHash.Parse("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Verify_PBKDF2SHA512_MissingIterations_ThrowsFormatException()
{
var hash = PasswordHash.Parse("$PBKDF2-SHA512$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Verify_PBKDF2_InvalidIterationsFormat_ThrowsFormatException()
{
var hash = PasswordHash.Parse("$PBKDF2$iterations=abc$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Verify_PBKDF2SHA512_InvalidIterationsFormat_ThrowsFormatException()
{
var hash = PasswordHash.Parse("$PBKDF2-SHA512$iterations=notanumber$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal);
}
[Fact]
public void Verify_UnsupportedHashId_ThrowsNotSupportedException()
{
var hash = PasswordHash.Parse("$UNKNOWN$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
Assert.Throws<NotSupportedException>(() => _sut.Verify(hash, "password"));
}
[Fact]
public void GenerateSalt_ReturnsNonEmptyArray()
{
var salt = _sut.GenerateSalt();
Assert.NotEmpty(salt);
}
[Theory]
[InlineData(16)]
[InlineData(32)]
[InlineData(64)]
public void GenerateSalt_WithLength_ReturnsArrayOfSpecifiedLength(int length)
{
var salt = _sut.GenerateSalt(length);
Assert.Equal(length, salt.Length);
}
}
@@ -0,0 +1,109 @@
using System;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller.Entities.TV;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Data
{
public class SearchPunctuationTests
{
private readonly IFixture _fixture;
private readonly BaseItemRepository _repo;
public SearchPunctuationTests()
{
var appHost = new Mock<MediaBrowser.Controller.IServerApplicationHost>();
appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
.Returns((string x) => x);
appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
.Returns((string x) => x);
var configSection = new Mock<IConfigurationSection>();
configSection
.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
.Returns("0");
var config = new Mock<IConfiguration>();
config
.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
.Returns(configSection.Object);
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_fixture.Inject(appHost.Object);
_fixture.Inject(config.Object);
_repo = _fixture.Create<BaseItemRepository>();
}
[Fact]
public void CleanName_keeps_punctuation_and_search_without_punctuation_passes()
{
var series = new Series
{
Id = Guid.NewGuid(),
Name = "Mr. Robot"
};
series.SortName = "Mr. Robot";
var entity = _repo.Map(series);
Assert.Equal("mr robot", entity.CleanName);
var searchTerm = "Mr Robot".ToLowerInvariant();
Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData("Spider-Man: Homecoming", "spider man homecoming")]
[InlineData("Beyoncé — Live!", "beyonce live")]
[InlineData("Hello, World!", "hello world")]
[InlineData("(The) Good, the Bad & the Ugly", "the good the bad the ugly")]
[InlineData("Wall-E", "wall e")]
[InlineData("No. 1: The Beginning", "no 1 the beginning")]
[InlineData("Café-au-lait", "cafe au lait")]
public void CleanName_normalizes_various_punctuation(string title, string expectedClean)
{
var series = new Series
{
Id = Guid.NewGuid(),
Name = title
};
series.SortName = title;
var entity = _repo.Map(series);
Assert.Equal(expectedClean, entity.CleanName);
// Ensure a search term without punctuation would match
var searchTerm = expectedClean;
Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData("Face/Off", "face off")]
[InlineData("V/H/S", "v h s")]
public void CleanName_normalizes_titles_withslashes(string title, string expectedClean)
{
var series = new Series
{
Id = Guid.NewGuid(),
Name = title
};
series.SortName = title;
var entity = _repo.Map(series);
Assert.Equal(expectedClean, entity.CleanName);
// Ensure a search term without punctuation would match
var searchTerm = expectedClean;
Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.Data;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Configuration;
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 BaseItemRepository _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));
var configSection = new Mock<IConfigurationSection>();
configSection
.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
.Returns("0");
var config = new Mock<IConfiguration>();
config
.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
.Returns(configSection.Object);
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_fixture.Inject(appHost);
_fixture.Inject(config);
_sqliteItemRepository = _fixture.Create<BaseItemRepository>();
}
public static TheoryData<string, ItemImageInfo> ItemImageInfoFromValueString_Valid_TestData()
{
var data = new TheoryData<string, ItemImageInfo>();
data.Add(
"/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"
});
data.Add(
"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,
});
data.Add(
"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,
});
data.Add(
"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,
});
data.Add(
"%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
});
return data;
}
public static TheoryData<string, ItemImageInfo[]> DeserializeImages_Valid_TestData()
{
var data = new TheoryData<string, ItemImageInfo[]>();
data.Add(
"/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"
}
});
data.Add(
"%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),
}
});
return data;
}
public static TheoryData<string, ItemImageInfo[]> DeserializeImages_ValidAndInvalid_TestData()
{
var data = new TheoryData<string, ItemImageInfo[]>();
data.Add(
string.Empty,
Array.Empty<ItemImageInfo>());
data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
new ItemImageInfo[]
{
new()
{
Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
Type = ImageType.Primary,
DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
});
data.Add(
"|",
Array.Empty<ItemImageInfo>());
return data;
}
private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds
{
public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
}
}
}
@@ -0,0 +1,17 @@
using Jellyfin.Database.Providers.Sqlite.Migrations;
using Jellyfin.Server.Implementations.Migrations;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.EfMigrations;
public class EfMigrationTests
{
[Fact]
public void CheckForUnappliedMigrations_SqLite()
{
var dbDesignContext = new SqliteDesignTimeJellyfinDbFactory();
var context = dbDesignContext.CreateDbContext([]);
Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EFCore model for SQLite. Please create a Migration.");
}
}
@@ -0,0 +1,69 @@
using System;
using System.Buffers;
using System.IO;
using System.Text.Json;
using Emby.Server.Implementations.HttpServer;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.HttpServer
{
public class WebSocketConnectionTests
{
[Fact]
public void DeserializeWebSocketMessage_SingleSegment_Success()
{
var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
var bytes = File.ReadAllBytes("Test Data/HttpServer/ForceKeepAlive.json");
con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed);
Assert.Equal(109, bytesConsumed);
}
[Fact]
public void DeserializeWebSocketMessage_MultipleSegments_Success()
{
const int SplitPos = 64;
var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
var bytes = File.ReadAllBytes("Test Data/HttpServer/ForceKeepAlive.json");
var seg1 = new BufferSegment(new Memory<byte>(bytes, 0, SplitPos));
var seg2 = seg1.Append(new Memory<byte>(bytes, SplitPos, bytes.Length - SplitPos));
con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(seg1, 0, seg2, seg2.Memory.Length - 1), out var bytesConsumed);
Assert.Equal(109, bytesConsumed);
}
[Fact]
public void DeserializeWebSocketMessage_ValidPartial_Success()
{
var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
var bytes = File.ReadAllBytes("Test Data/HttpServer/ValidPartial.json");
con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed);
Assert.Equal(109, bytesConsumed);
}
[Fact]
public void DeserializeWebSocketMessage_Partial_ThrowJsonException()
{
var con = new WebSocketConnection(new NullLogger<WebSocketConnection>(), null!, null!, null!);
var bytes = File.ReadAllBytes("Test Data/HttpServer/Partial.json");
Assert.Throws<JsonException>(() => con.DeserializeWebSocketMessage(new ReadOnlySequence<byte>(bytes), out var bytesConsumed));
}
internal sealed class BufferSegment : ReadOnlySequenceSegment<byte>
{
public BufferSegment(Memory<byte> memory)
{
Memory = memory;
}
public BufferSegment Append(Memory<byte> memory)
{
var segment = new BufferSegment(memory)
{
RunningIndex = RunningIndex + Memory.Length
};
Next = segment;
return segment;
}
}
}
}
@@ -0,0 +1,123 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.IO;
using Jellyfin.Extensions;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.IO;
public class ManagedFileSystemTests
{
private readonly IFixture _fixture;
private readonly ManagedFileSystem _sut;
public ManagedFileSystemTests()
{
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_sut = _fixture.Create<ManagedFileSystem>();
}
[Fact]
public void MoveDirectory_SameFileSystem_Correct()
=> MoveDirectoryInternal();
[SkippableFact]
public void MoveDirectory_DifferentFileSystem_Correct()
{
const string DestinationParent = "/dev/shm";
Skip.IfNot(Directory.Exists(DestinationParent));
MoveDirectoryInternal(DestinationParent);
}
internal void MoveDirectoryInternal(string? destinationParent = null)
{
const string TempFile0 = "tempfile0";
const string TempFile1 = "tempfile1";
destinationParent ??= Path.GetTempPath();
var sourceDir = Directory.CreateTempSubdirectory();
var destinationDir = Path.Join(destinationParent, Path.GetRandomFileName());
FileHelper.CreateEmpty(Path.Join(sourceDir.FullName, TempFile0));
FileHelper.CreateEmpty(Path.Join(sourceDir.FullName, TempFile1));
_sut.MoveDirectory(sourceDir.FullName, destinationDir);
Assert.True(Directory.Exists(destinationDir));
Assert.True(File.Exists(Path.Join(destinationDir, TempFile0)));
Assert.True(File.Exists(Path.Join(destinationDir, TempFile1)));
Assert.False(Directory.Exists(sourceDir.FullName));
Directory.Delete(destinationDir, true);
}
[SkippableTheory]
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Beethoven/Misc/Moonlight Sonata.mp3")]
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "../../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Beethoven/Misc/Moonlight Sonata.mp3")]
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Playlists/Beethoven/Misc/Moonlight Sonata.mp3")]
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3")]
public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnUnixLike(
string folderPath,
string filePath,
string expectedAbsolutePath)
{
Skip.If(OperatingSystem.IsWindows());
var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
Assert.Equal(expectedAbsolutePath, generatedPath);
}
[SkippableTheory]
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Beethoven\Misc\Moonlight Sonata.mp3")]
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Beethoven\Misc\Moonlight Sonata.mp3")]
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Playlists\Beethoven\Misc\Moonlight Sonata.mp3")]
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3")]
public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnWindows(
string folderPath,
string filePath,
string expectedAbsolutePath)
{
Skip.IfNot(OperatingSystem.IsWindows());
var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
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(OperatingSystem.IsWindows());
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);
}
@@ -0,0 +1,72 @@
using System;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Item;
public class BaseItemRepositoryTests
{
[Fact]
public void DeserializeBaseItem_WithUnknownType_ReturnsNull()
{
// Arrange
var entity = new BaseItemEntity
{
Id = Guid.NewGuid(),
Type = "NonExistent.Plugin.CustomItemType"
};
// Act
var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
// Assert
Assert.Null(result);
}
[Fact]
public void DeserializeBaseItem_WithUnknownType_LogsWarning()
{
// Arrange
var entity = new BaseItemEntity
{
Id = Guid.NewGuid(),
Type = "NonExistent.Plugin.CustomItemType"
};
var loggerMock = new Mock<ILogger>();
// Act
BaseItemRepository.DeserializeBaseItem(entity, loggerMock.Object, null, false);
// Assert
loggerMock.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unknown type", StringComparison.OrdinalIgnoreCase)),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public void DeserializeBaseItem_WithKnownType_ReturnsItem()
{
// Arrange
var entity = new BaseItemEntity
{
Id = Guid.NewGuid(),
Type = "MediaBrowser.Controller.Entities.Movies.Movie"
};
// Act
var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
// Assert
Assert.NotNull(result);
}
}
@@ -0,0 +1,35 @@
using System;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller.Entities;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Item;
public class OrderMapperTests
{
[Fact]
public void ShouldReturnMappedOrderForSortingByPremierDate()
{
var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery(), null!).Compile();
var expectedDate = new DateTime(1, 2, 3);
var expectedProductionYearDate = new DateTime(4, 1, 1);
var entityWithOnlyProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", ProductionYear = expectedProductionYearDate.Year };
var entityWithOnlyPremierDate = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", PremiereDate = expectedDate };
var entityWithBothPremierDateAndProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", PremiereDate = expectedDate, ProductionYear = expectedProductionYearDate.Year };
var entityWithoutEitherPremierDateOrProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test" };
var resultWithOnlyProductionYear = orderFunc(entityWithOnlyProductionYear);
var resultWithOnlyPremierDate = orderFunc(entityWithOnlyPremierDate);
var resultWithBothPremierDateAndProductionYear = orderFunc(entityWithBothPremierDateAndProductionYear);
var resultWithoutEitherPremierDateOrProductionYear = orderFunc(entityWithoutEitherPremierDateOrProductionYear);
Assert.Equal(resultWithOnlyProductionYear, expectedProductionYearDate);
Assert.Equal(resultWithOnlyPremierDate, expectedDate);
Assert.Equal(resultWithBothPremierDateAndProductionYear, expectedDate);
Assert.Null(resultWithoutEitherPremierDateOrProductionYear);
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" />
<PackageReference Include="AutoFixture.AutoMoq" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
<ProjectReference Include="..\..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,77 @@
using System.Linq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.Audio;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class AudioResolverTests
{
private static readonly NamingOptions _namingOptions = new();
[Theory]
[InlineData("words.mp3")] // single non-tagged file
[InlineData("chapter 01.mp3")]
[InlineData("part 1.mp3")]
[InlineData("chapter 01.mp3", "non-media.txt")]
[InlineData("title.mp3", "title.epub")]
[InlineData("01.mp3", "subdirectory/")] // single media file with sub-directory - note that this will hide any contents in the subdirectory
public void Resolve_AudiobookDirectory_SingleResult(params string[] children)
{
var resolved = TestResolveChildren("/parent/title", children);
Assert.NotNull(resolved);
}
[Theory]
/* Results that can't be displayed as an audio book. */
[InlineData] // no contents
[InlineData("subdirectory/")]
[InlineData("non-media.txt")]
/* Names don't indicate parts of a single book. */
[InlineData("Name.mp3", "Another Name.mp3")]
/* Results that are an audio book but not currently navigable as such (multiple chapters and/or parts). */
[InlineData("01.mp3", "02.mp3")]
[InlineData("chapter 01.mp3", "chapter 02.mp3")]
[InlineData("part 1.mp3", "part 2.mp3")]
[InlineData("chapter 01 part 01.mp3", "chapter 01 part 02.mp3")]
/* Mismatched chapters, parts, and named files. */
[InlineData("chapter 01.mp3", "part 2.mp3")]
[InlineData("book title.mp3", "chapter name.mp3")] // "book title" resolves as alternate version of book based on directory name
[InlineData("01 Content.mp3", "01 Credits.mp3")] // resolves as alternate versions of chapter 1
[InlineData("Chapter Name.mp3", "Part 1.mp3")]
public void Resolve_AudiobookDirectory_NoResult(params string[] children)
{
var resolved = TestResolveChildren("/parent/book title", children);
Assert.Null(resolved);
}
private Audio? TestResolveChildren(string parent, string[] children)
{
var childrenMetadata = children.Select(name => new FileSystemMetadata
{
FullName = parent + "/" + name,
IsDirectory = name.EndsWith('/')
}).ToArray();
var resolver = new AudioResolver(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
null,
Mock.Of<ILibraryManager>())
{
CollectionType = CollectionType.books,
FileInfo = new FileSystemMetadata
{
FullName = parent,
IsDirectory = true
},
FileSystemChildren = childrenMetadata
};
return resolver.Resolve(itemResolveArgs);
}
}
@@ -0,0 +1,129 @@
using System;
using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class CoreResolutionIgnoreRuleTest
{
private readonly CoreResolutionIgnoreRule _rule;
private readonly NamingOptions _namingOptions;
private readonly Mock<IServerApplicationPaths> _appPathsMock;
public CoreResolutionIgnoreRuleTest()
{
_namingOptions = new NamingOptions();
_namingOptions.AllExtrasTypesFolderNames.TryAdd("extras", ExtraType.Trailer);
_appPathsMock = new Mock<IServerApplicationPaths>();
_appPathsMock.SetupGet(x => x.RootFolderPath).Returns("/server/root");
_rule = new CoreResolutionIgnoreRule(_namingOptions, _appPathsMock.Object);
}
private FileSystemMetadata MakeFileSystemMetadata(string fullName, bool isDirectory = false)
=> new FileSystemMetadata { FullName = fullName, Name = Path.GetFileName(fullName), IsDirectory = isDirectory };
private BaseItem MakeParent(string name = "Parent", bool isTopParent = false, Type? type = null)
{
return type switch
{
Type t when t == typeof(Folder) => CreateMock<Folder>(name, isTopParent).Object,
Type t when t == typeof(AggregateFolder) => CreateMock<AggregateFolder>(name, isTopParent).Object,
Type t when t == typeof(UserRootFolder) => CreateMock<UserRootFolder>(name, isTopParent).Object,
_ => CreateMock<BaseItem>(name, isTopParent).Object
};
}
private static Mock<T> CreateMock<T>(string name, bool isTopParent)
where T : BaseItem
{
var mock = new Mock<T>();
mock.SetupGet(p => p.Name).Returns(name);
mock.SetupGet(p => p.IsTopParent).Returns(isTopParent);
return mock;
}
[Fact]
public void TestApplicationFolder()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/server/root/extras", isDirectory: true),
null));
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/server/root/small.jpg"),
null));
}
[Fact]
public void TestTopLevelDirectory()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("Series/Extras", true),
MakeParent(type: typeof(AggregateFolder))));
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("Series/Extras/Extras", true),
MakeParent(isTopParent: true)));
}
[Fact]
public void TestIgnorePatterns()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Media/big.jpg"),
MakeParent()));
Assert.True(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Media/small.jpg"),
MakeParent()));
}
[Fact]
public void TestExtrasTypesFolderNames()
{
FileSystemMetadata fileSystemMetadata = MakeFileSystemMetadata("/Movies/Up/extras", true);
Assert.False(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent(type: typeof(AggregateFolder))));
Assert.False(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent(type: typeof(UserRootFolder))));
Assert.False(_rule.ShouldIgnore(
fileSystemMetadata,
null));
Assert.True(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent()));
Assert.True(_rule.ShouldIgnore(
fileSystemMetadata,
MakeParent(type: typeof(Folder))));
}
[Fact]
public void TestThemeSong()
{
Assert.False(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Movies/Up/intro.mp3"),
MakeParent()));
Assert.True(_rule.ShouldIgnore(
MakeFileSystemMetadata("/Movies/Up/theme.mp3"),
MakeParent()));
}
}
@@ -0,0 +1,81 @@
using Emby.Server.Implementations.Library;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class DotIgnoreIgnoreRuleTest
{
private static readonly string[] _rule1 = ["SPs"];
private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"];
private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"];
private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"];
public static TheoryData<string[], string, bool, bool> CheckIgnoreRulesTestData =>
new()
{
// Basic pattern matching
{ _rule1, "f:/cd/sps/ffffff.mkv", false, true },
{ _rule1, "cd/sps/ffffff.mkv", false, true },
{ _rule1, "/cd/sps/ffffff.mkv", false, true },
// Negate pattern
{ _rule2, "f:/cd/sps/ffffff.mkv", false, true },
{ _rule2, "cd/sps/ffffff.mkv", false, true },
{ _rule2, "/cd/sps/ffffff.mkv", false, true },
{ _rule2, "f:/cd/sps/thebestshot.mkv", false, false },
{ _rule2, "cd/sps/thebestshot.mkv", false, false },
{ _rule2, "/cd/sps/thebestshot.mkv", false, false },
// Mixed valid and invalid patterns - skips invalid, applies valid
{ _rule3, "test.txt", false, true },
{ _rule3, "videos/movie.mp4", false, true },
{ _rule3, "movie.mkv", false, true },
{ _rule3, "test.mp3", false, false },
// Only invalid patterns - falls back to ignore all
{ _rule4, "any-file.txt", false, true },
{ _rule4, "any/path/to/file.mkv", false, true },
};
public static TheoryData<string[], string, bool, bool> WindowsPathNormalizationTestData =>
new()
{
// Windows paths with backslashes - should match when normalizePath is true
{ _rule1, @"C:\cd\sps\ffffff.mkv", false, true },
{ _rule1, @"D:\media\sps\movie.mkv", false, true },
{ _rule1, @"\\server\share\sps\file.mkv", false, true },
// Negate pattern with Windows paths
{ _rule2, @"C:\cd\sps\ffffff.mkv", false, true },
{ _rule2, @"C:\cd\sps\thebestshot.mkv", false, false },
// Directory matching with Windows paths
{ _rule3, @"C:\videos\movie.mp4", false, true },
{ _rule3, @"D:\documents\test.txt", false, true },
{ _rule3, @"E:\music\song.mp3", false, false },
};
[Theory]
[MemberData(nameof(CheckIgnoreRulesTestData))]
public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored)
{
Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory));
}
[Theory]
[MemberData(nameof(WindowsPathNormalizationTestData))]
public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored)
{
// With normalizePath=true, backslashes should be converted to forward slashes
Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true));
}
[Theory]
[InlineData(@"C:\cd\sps\ffffff.mkv")]
[InlineData(@"D:\media\sps\movie.mkv")]
public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path)
{
// Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes
Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false));
}
}
@@ -0,0 +1,73 @@
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.TV;
using Jellyfin.Data.Enums;
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 Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library
{
public class EpisodeResolverTest
{
private static readonly NamingOptions _namingOptions = new();
[Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{
var parent = new Folder { Name = "extras" };
var episodeResolver = new EpisodeResolver(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
null)
{
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<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
null)
{
Parent = series,
CollectionType = CollectionType.tvshows,
FileInfo = new FileSystemMetadata
{
FullName = "Extras/Extras S01E01.mkv"
}
};
Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
}
private sealed class EpisodeResolverMock : EpisodeResolver
{
public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService)
{
}
protected override TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) => new();
}
}
}
@@ -0,0 +1,40 @@
using Emby.Server.Implementations.Library;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library
{
public class IgnorePatternsTests
{
[Theory]
[InlineData("/media/small.jpg", true)]
[InlineData("/media/albumart.jpg", true)]
[InlineData("/media/movie.sample.mp4", true)]
[InlineData("/media/movie/sample.mp4", true)]
[InlineData("/media/movie/sample/movie.mp4", true)]
[InlineData("/foo/sample/bar/baz.mkv", false)]
[InlineData("/media/movies/the sample/the sample.mkv", false)]
[InlineData("/media/movies/sampler.mkv", false)]
[InlineData("/media/movies/#Recycle/test.txt", true)]
[InlineData("/media/movies/#recycle/", true)]
[InlineData("/media/movies/#recycle", true)]
[InlineData("thumbs.db", true)]
[InlineData(@"C:\media\movies\movie.avi", false)]
[InlineData("/media/.hiddendir/file.mp4", false)]
[InlineData("/media/dir/.hiddenfile.mp4", true)]
[InlineData("/media/dir/._macjunk.mp4", true)]
[InlineData("/volume1/video/Series/@eaDir", true)]
[InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
[InlineData("/directory/@Recycle", true)]
[InlineData("/directory/@Recycle/file.mp3", true)]
[InlineData("/media/movies/.@__thumb", true)]
[InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)]
[InlineData("/media/music/Foo B.A.R./epic.flac", false)]
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
}
}
}
@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.Audio;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library.LibraryManager;
public class FindExtrasTests
{
private readonly Emby.Server.Implementations.Library.LibraryManager _libraryManager;
private readonly Mock<IFileSystem> _fileSystemMock;
public FindExtrasTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
fixture.Register(() => new NamingOptions());
var configMock = fixture.Freeze<Mock<IServerConfigurationManager>>();
configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data");
var itemRepository = fixture.Freeze<Mock<IItemRepository>>();
itemRepository.Setup(i => i.RetrieveItem(It.IsAny<Guid>())).Returns<BaseItem>(null);
_fileSystemMock = fixture.Freeze<Mock<IFileSystem>>();
_fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny<string>())).Returns<string>(path => new FileSystemMetadata { FullName = path });
_libraryManager = fixture.Build<Emby.Server.Implementations.Library.LibraryManager>().Do(s => s.AddParts(
fixture.Create<IEnumerable<IResolverIgnoreRule>>(),
new List<IItemResolver> { new AudioResolver(fixture.Create<NamingOptions>()) },
fixture.Create<IEnumerable<IIntroProvider>>(),
fixture.Create<IEnumerable<IBaseItemComparer>>(),
fixture.Create<IEnumerable<ILibraryPostScanTask>>()))
.Create();
// This is pretty terrible but unavoidable
BaseItem.FileSystem ??= fixture.Create<IFileSystem>();
BaseItem.MediaSourceManager ??= fixture.Create<IMediaSourceManager>();
}
[Fact]
public void FindExtras_SeparateMovieFolder_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/Up - trailer.mkv",
"/movies/Up/Up - sample.mkv",
"/movies/Up/Up something else.mkv",
"/movies/Up/Up-extra.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
Assert.Equal(3, extras.Count);
Assert.Equal(ExtraType.Unknown, extras[0].ExtraType);
Assert.Equal(ExtraType.Trailer, extras[1].ExtraType);
Assert.Equal(typeof(Trailer), extras[1].GetType());
Assert.Equal(ExtraType.Sample, extras[2].ExtraType);
}
[Fact]
public void FindExtras_SeparateMovieFolder_CleanExtraNames()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/Recording the audio[Bluray]-behindthescenes.mkv",
"/movies/Up/Interview with the dog-interview.mkv",
"/movies/Up/shorts/Balloons[1080p].mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
Assert.Equal(3, extras.Count);
Assert.Equal(ExtraType.BehindTheScenes, extras[0].ExtraType);
Assert.Equal("Recording the audio", extras[0].Name);
Assert.Equal(ExtraType.Interview, extras[1].ExtraType);
Assert.Equal("Interview with the dog", extras[1].Name);
Assert.Equal(ExtraType.Short, extras[2].ExtraType);
Assert.Equal("Balloons", extras[2].Name);
}
[Fact]
public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/Up - trailer.mkv",
"/movies/Up/trailers",
"/movies/Up/theme-music",
"/movies/Up/theme.mp3",
"/movies/Up/not a theme.mp3",
"/movies/Up/behind the scenes",
"/movies/Up/behind the scenes.mkv",
"/movies/Up/Up - sample.mkv",
"/movies/Up/Up something else.mkv",
"/movies/Up/extras"
};
_fileSystemMock.Setup(f => f.GetFiles(
"/movies/Up/trailers",
It.IsAny<string[]>(),
false,
false))
.Returns(new List<FileSystemMetadata>
{
new()
{
FullName = "/movies/Up/trailers/some trailer.mkv",
Name = "some trailer.mkv",
IsDirectory = false
}
}).Verifiable();
_fileSystemMock.Setup(f => f.GetFiles(
"/movies/Up/behind the scenes",
It.IsAny<string[]>(),
false,
false))
.Returns(new List<FileSystemMetadata>
{
new()
{
FullName = "/movies/Up/behind the scenes/the making of Up.mkv",
Name = "the making of Up.mkv",
IsDirectory = false
}
}).Verifiable();
_fileSystemMock.Setup(f => f.GetFiles(
"/movies/Up/theme-music",
It.IsAny<string[]>(),
false,
false))
.Returns(new List<FileSystemMetadata>
{
new()
{
FullName = "/movies/Up/theme-music/theme2.mp3",
Name = "theme2.mp3",
IsDirectory = false
}
}).Verifiable();
_fileSystemMock.Setup(f => f.GetFiles(
"/movies/Up/extras",
It.IsAny<string[]>(),
false,
false))
.Returns(new List<FileSystemMetadata>
{
new()
{
FullName = "/movies/Up/extras/Honest Trailer.mkv",
Name = "Honest Trailer.mkv",
IsDirectory = false
}
}).Verifiable();
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
Name = Path.GetFileName(p),
IsDirectory = !Path.HasExtension(p)
}).ToList();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
_fileSystemMock.Verify();
Assert.Equal(7, extras.Count);
Assert.Equal(ExtraType.Unknown, extras[0].ExtraType);
Assert.Equal(typeof(Video), extras[0].GetType());
Assert.Equal(ExtraType.Trailer, extras[1].ExtraType);
Assert.Equal(typeof(Trailer), extras[1].GetType());
Assert.Equal(ExtraType.Trailer, extras[2].ExtraType);
Assert.Equal(typeof(Trailer), extras[2].GetType());
Assert.Equal(ExtraType.BehindTheScenes, extras[3].ExtraType);
Assert.Equal(ExtraType.Sample, extras[4].ExtraType);
Assert.Equal(ExtraType.ThemeSong, extras[5].ExtraType);
Assert.Equal(typeof(Audio), extras[5].GetType());
Assert.Equal(ExtraType.ThemeSong, extras[6].ExtraType);
Assert.Equal(typeof(Audio), extras[6].GetType());
}
[Fact]
public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsOnlyExtrasInMovieFolder()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/trailer.mkv",
"/movies/Another Movie/trailer.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
Assert.Single(extras);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal(typeof(Trailer), extras[0].GetType());
Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
Assert.Equal("/movies/Up/trailer.mkv", extras[0].Path);
}
[Fact]
public void FindExtras_SeparateMovieFolderWithParts_FindsCorrectExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up - part1.mkv" };
var paths = new List<string>
{
"/movies/Up/Up - part1.mkv",
"/movies/Up/Up - part2.mkv",
"/movies/Up/trailer.mkv",
"/movies/Another Movie/trailer.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = false
}).ToList();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
Assert.Single(extras);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal(typeof(Trailer), extras[0].GetType());
Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
Assert.Equal("/movies/Up/trailer.mkv", extras[0].Path);
}
[Fact]
public void FindExtras_WrongExtensions_FindsNoExtras()
{
var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
var paths = new List<string>
{
"/movies/Up/Up.mkv",
"/movies/Up/trailer.noext",
"/movies/Up/theme.png",
"/movies/Up/trailers"
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
Name = Path.GetFileName(p),
IsDirectory = !Path.HasExtension(p)
}).ToList();
_fileSystemMock.Setup(f => f.GetFiles(
"/movies/Up/trailers",
It.IsAny<string[]>(),
false,
false))
.Returns(new List<FileSystemMetadata>
{
new()
{
FullName = "/movies/Up/trailers/trailer.jpg",
Name = "trailer.jpg",
IsDirectory = false
}
}).Verifiable();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
_fileSystemMock.Verify();
Assert.Empty(extras);
}
[Fact]
public void FindExtras_SeriesWithTrailers_FindsCorrectExtras()
{
var owner = new Series { Name = "Dexter", Path = "/series/Dexter" };
var paths = new List<string>
{
"/series/Dexter/Season 1/S01E01.mkv",
"/series/Dexter/trailer.mkv",
"/series/Dexter/trailers/trailer2.mkv",
};
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p))
}).ToList();
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
Assert.Equal(2, extras.Count);
Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
Assert.Equal(typeof(Trailer), extras[0].GetType());
Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
Assert.Equal("/series/Dexter/trailer.mkv", extras[0].Path);
Assert.Equal("/series/Dexter/trailers/trailer2.mkv", extras[1].Path);
}
}
@@ -0,0 +1,32 @@
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library
{
public class MediaSourceManagerTests
{
private readonly MediaSourceManager _mediaSourceManager;
public MediaSourceManagerTests()
{
IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
fixture.Inject<IFileSystem>(fixture.Create<ManagedFileSystem>());
_mediaSourceManager = fixture.Create<MediaSourceManager>();
}
[Theory]
[InlineData(@"C:\mydir\myfile.ext", MediaProtocol.File)]
[InlineData("/mydir/myfile.ext", MediaProtocol.File)]
[InlineData("file:///mydir/myfile.ext", MediaProtocol.File)]
[InlineData("http://example.com/stream.m3u8", MediaProtocol.Http)]
[InlineData("https://example.com/stream.m3u8", MediaProtocol.Http)]
[InlineData("rtsp://media.example.com:554/twister/audiotrack", MediaProtocol.Rtsp)]
public void GetPathProtocol_ValidArg_Correct(string path, MediaProtocol expected)
=> Assert.Equal(expected, _mediaSourceManager.GetPathProtocol(path));
}
}
@@ -0,0 +1,118 @@
using System;
using Emby.Server.Implementations.Library;
using MediaBrowser.Model.Entities;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class MediaStreamSelectorTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void GetDefaultAudioStreamIndex_EmptyStreams_Null(bool preferDefaultTrack)
{
Assert.Null(MediaStreamSelector.GetDefaultAudioStreamIndex(Array.Empty<MediaStream>(), Array.Empty<string>(), preferDefaultTrack));
}
[Theory]
[InlineData(new string[0], false, 1)]
[InlineData(new string[0], true, 1)]
[InlineData(new[] { "eng" }, false, 2)]
[InlineData(new[] { "eng" }, true, 1)]
[InlineData(new[] { "eng", "fre" }, false, 2)]
[InlineData(new[] { "fre", "eng" }, false, 1)]
[InlineData(new[] { "eng", "fre" }, true, 1)]
public void GetDefaultAudioStreamIndex_PreferredLanguage_SelectsCorrect(string[] preferredLanguages, bool preferDefaultTrack, int expectedIndex)
{
var streams = new MediaStream[]
{
new()
{
Index = 0,
Type = MediaStreamType.Video,
IsDefault = true
},
new()
{
Index = 1,
Type = MediaStreamType.Audio,
Language = "fre",
IsDefault = true
},
new()
{
Index = 2,
Type = MediaStreamType.Audio,
Language = "eng",
IsDefault = false
}
};
Assert.Equal(expectedIndex, MediaStreamSelector.GetDefaultAudioStreamIndex(streams, preferredLanguages, preferDefaultTrack));
}
public static TheoryData<MediaStream, int> GetStreamScore_MediaStream_TestData()
{
var data = new TheoryData<MediaStream, int>();
data.Add(new MediaStream(), 111111);
data.Add(
new MediaStream()
{
Language = "eng"
},
10111111);
data.Add(
new MediaStream()
{
Language = "fre"
},
10011111);
data.Add(
new MediaStream()
{
IsForced = true
},
121111);
data.Add(
new MediaStream()
{
IsDefault = true
},
112111);
data.Add(
new MediaStream()
{
SupportsExternalStream = true
},
111211);
data.Add(
new MediaStream()
{
IsExternal = true
},
111112);
data.Add(
new MediaStream()
{
Language = "eng",
IsForced = true,
IsDefault = true,
SupportsExternalStream = true,
IsExternal = true
},
10122212);
return data;
}
[Theory]
[MemberData(nameof(GetStreamScore_MediaStream_TestData))]
public void GetStreamScore_MediaStream_CorrectScore(MediaStream stream, int expectedScore)
{
var languagePref = new[] { "eng", "fre" };
Assert.Equal(expectedScore, MediaStreamSelector.GetStreamScore(stream, languagePref));
}
}
@@ -0,0 +1,35 @@
using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.Movies;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class MovieResolverTests
{
private static readonly NamingOptions _namingOptions = new();
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
null)
{
Parent = null,
FileInfo = new FileSystemMetadata
{
FullName = "/movies/Black Panther (2018)/Black Panther (2018) - 1080p 3D.mk3d"
}
};
Assert.NotNull(movieResolver.Resolve(itemResolveArgs));
}
}
@@ -0,0 +1,137 @@
using System;
using System.IO;
using Emby.Server.Implementations.Library;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library
{
public class PathExtensionsTests
{
[Theory]
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
[InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("{tmdbid=618355}", "tmdbid", "618355")]
[InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("{tmdbid-618355)", "tmdbid", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)]
[InlineData("[tmdbid=618355", "tmdbid", null)]
[InlineData("tmdbid=618355", "tmdbid", null)]
[InlineData("tmdbid=", "tmdbid", null)]
[InlineData("tmdbid", "tmdbid", null)]
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));
}
[Theory]
[InlineData("", "")]
[InlineData("Superman: Red Son [imdbid=tt10985510]", "")]
[InlineData("", "imdbid")]
public void GetAttributeValue_EmptyString_ThrowsArgumentException(string input, string attribute)
{
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);
}
[Theory]
[InlineData(null, '/', null)]
[InlineData(null, '\\', null)]
[InlineData("/home/jeff/myfile.mkv", '\\', @"\home\jeff\myfile.mkv")]
[InlineData(@"C:\Users\Jeff\myfile.mkv", '/', "C:/Users/Jeff/myfile.mkv")]
[InlineData(@"\home/jeff\myfile.mkv", '\\', @"\home\jeff\myfile.mkv")]
[InlineData(@"\home/jeff\myfile.mkv", '/', "/home/jeff/myfile.mkv")]
[InlineData("", '/', "")]
public void NormalizePath_SpecifyingSeparator_Normalizes(string? path, char separator, string? expectedPath)
{
Assert.Equal(expectedPath, path.NormalizePath(separator));
}
[Theory]
[InlineData("/home/jeff/myfile.mkv")]
[InlineData(@"C:\Users\Jeff\myfile.mkv")]
[InlineData(@"\home/jeff\myfile.mkv")]
public void NormalizePath_NoArgs_UsesDirectorySeparatorChar(string path)
{
var separator = Path.DirectorySeparatorChar;
Assert.Equal(path.Replace('\\', separator).Replace('/', separator), path.NormalizePath());
}
[Theory]
[InlineData("/home/jeff/myfile.mkv", '/')]
[InlineData(@"C:\Users\Jeff\myfile.mkv", '\\')]
[InlineData(@"\home/jeff\myfile.mkv", '/')]
public void NormalizePath_OutVar_Correct(string path, char expectedSeparator)
{
var result = path.NormalizePath(out var separator);
Assert.Equal(expectedSeparator, separator);
Assert.Equal(path.Replace('\\', separator).Replace('/', separator), result);
}
[Fact]
public void NormalizePath_SpecifyInvalidSeparator_ThrowsException()
{
Assert.Throws<ArgumentException>(() => string.Empty.NormalizePath('a'));
}
}
}
@@ -0,0 +1,263 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BitFaster.Caching;
using Emby.Server.Implementations.Localization;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Localization
{
public class LocalizationManagerTests
{
[Fact]
public void GetCountries_All_Success()
{
var localizationManager = Setup(new ServerConfiguration
{
UICulture = "de-DE"
});
var countries = localizationManager.GetCountries().ToList();
Assert.Equal(139, countries.Count);
var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal));
Assert.NotNull(germany);
Assert.Equal("Germany", germany!.DisplayName);
Assert.Equal("DEU", germany.ThreeLetterISORegionName);
Assert.Equal("DE", germany.TwoLetterISORegionName);
}
[Fact]
public async Task GetCultures_All_Success()
{
var localizationManager = Setup(new ServerConfiguration
{
UICulture = "de-DE"
});
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
Assert.Equal(194, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
Assert.Equal("deu", germany!.ThreeLetterISOLanguageName);
Assert.Equal("German", germany.DisplayName);
Assert.Equal("German", germany.Name);
Assert.Contains("deu", germany.ThreeLetterISOLanguageNames);
Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
}
[Fact]
public async Task TryGetISO6392TFromB_Success()
{
var localizationManager = Setup(new ServerConfiguration
{
UICulture = "de-DE"
});
await localizationManager.LoadAll();
string? isoT;
// Translation ger -> deu
Assert.True(localizationManager.TryGetISO6392TFromB("ger", out isoT));
Assert.Equal("deu", isoT);
// chi -> zho
Assert.True(localizationManager.TryGetISO6392TFromB("chi", out isoT));
Assert.Equal("zho", isoT);
// eng is already ISO 639-2/T
Assert.False(localizationManager.TryGetISO6392TFromB("eng", out isoT));
Assert.Null(isoT);
}
[Theory]
[InlineData("de")]
[InlineData("deu")]
[InlineData("ger")]
[InlineData("german")]
public async Task FindLanguageInfo_Valid_Success(string identifier)
{
var localizationManager = Setup(new ServerConfiguration
{
UICulture = "de-DE"
});
await localizationManager.LoadAll();
var germany = localizationManager.FindLanguageInfo(identifier);
Assert.NotNull(germany);
Assert.Equal("deu", germany!.ThreeLetterISOLanguageName);
Assert.Equal("German", germany.DisplayName);
Assert.Equal("German", germany.Name);
Assert.Contains("deu", germany.ThreeLetterISOLanguageNames);
Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
}
[Fact]
public async Task GetParentalRatings_Default_Success()
{
var localizationManager = Setup(new ServerConfiguration
{
UICulture = "de-DE"
});
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(56, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
Assert.Equal(17, tvma!.RatingScore!.Score);
}
[Fact]
public async Task GetParentalRatings_ConfiguredCountryCode_Success()
{
var localizationManager = Setup(new ServerConfiguration()
{
MetadataCountryCode = "DE"
});
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(24, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);
Assert.Equal(12, fsk!.RatingScore!.Score);
}
[Theory]
[InlineData("CA-R", "CA", 18, 1)]
[InlineData("FSK-16", "DE", 16, null)]
[InlineData("FSK-18", "DE", 18, null)]
[InlineData("FSK-18", "US", 18, null)]
[InlineData("TV-MA", "US", 17, 1)]
[InlineData("XXX", "asdf", 1000, null)]
[InlineData("Germany: FSK-18", "DE", 18, null)]
[InlineData("Rated : R", "US", 17, 0)]
[InlineData("Rated: R", "US", 17, 0)]
[InlineData("Rated R", "US", 17, 0)]
[InlineData(" PG-13 ", "US", 13, 0)]
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int? expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration()
{
MetadataCountryCode = countryCode
});
await localizationManager.LoadAll();
var score = localizationManager.GetRatingScore(value);
Assert.NotNull(score);
Assert.Equal(expectedScore, score.Score);
Assert.Equal(expectedSubScore, score.SubScore);
}
[Theory]
[InlineData("0", 0, null)]
[InlineData("1", 1, null)]
[InlineData("6", 6, null)]
[InlineData("12", 12, null)]
[InlineData("42", 42, null)]
[InlineData("9999", 9999, null)]
public async Task GetRatingLevel_GivenValidAge_Success(string value, int? expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "nl" });
await localizationManager.LoadAll();
var score = localizationManager.GetRatingScore(value);
Assert.NotNull(score);
Assert.Equal(expectedScore, score.Score);
Assert.Equal(expectedSubScore, score.SubScore);
}
[Fact]
public async Task GetRatingLevel_GivenUnratedString_Success()
{
var localizationManager = Setup(new ServerConfiguration()
{
UICulture = "de-DE"
});
await localizationManager.LoadAll();
Assert.Null(localizationManager.GetRatingScore("NR"));
Assert.Null(localizationManager.GetRatingScore("unrated"));
Assert.Null(localizationManager.GetRatingScore("Not Rated"));
Assert.Null(localizationManager.GetRatingScore("n/a"));
}
[Theory]
[InlineData("-NO RATING SHOWN-")]
[InlineData(":NO RATING SHOWN:")]
public async Task GetRatingLevel_Split_Success(string value)
{
var localizationManager = Setup(new ServerConfiguration()
{
UICulture = "en-US"
});
await localizationManager.LoadAll();
Assert.Null(localizationManager.GetRatingScore(value));
}
[Theory]
[InlineData("TV-MA", "DE", 17, 1)] // US-only rating, DE country code
[InlineData("PG-13", "FR", 13, 0)] // US-only rating, FR country code
[InlineData("R", "JP", 17, 0)] // US-only rating, JP country code
public async Task GetRatingScore_FallbackPrioritizesUS_Success(string rating, string countryCode, int expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration()
{
MetadataCountryCode = countryCode
});
await localizationManager.LoadAll();
var score = localizationManager.GetRatingScore(rating);
Assert.NotNull(score);
Assert.Equal(expectedScore, score.Score);
Assert.Equal(expectedSubScore, score.SubScore);
}
[Theory]
[InlineData("Default", "Default")]
[InlineData("HeaderLiveTV", "Live TV")]
public void GetLocalizedString_Valid_Success(string key, string expected)
{
var localizationManager = Setup(new ServerConfiguration()
{
UICulture = "en-US"
});
var translated = localizationManager.GetLocalizedString(key);
Assert.NotNull(translated);
Assert.Equal(expected, translated);
}
[Fact]
public void GetLocalizedString_Invalid_Success()
{
var localizationManager = Setup(new ServerConfiguration()
{
UICulture = "en-US"
});
var key = "SuperInvalidTranslationKeyThatWillNeverBeAdded";
var translated = localizationManager.GetLocalizedString(key);
Assert.NotNull(translated);
Assert.Equal(key, translated);
}
private LocalizationManager Setup(ServerConfiguration config)
{
var mockConfiguration = new Mock<IServerConfigurationManager>();
mockConfiguration.SetupGet(x => x.Configuration).Returns(config);
return new LocalizationManager(mockConfiguration.Object, new NullLogger<LocalizationManager>());
}
}
}
@@ -0,0 +1,40 @@
using Emby.Server.Implementations.Playlists;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Playlists;
public class PlaylistManagerTests
{
[Fact]
public void DetermineAdjustedIndexMoveToFirstPositionNoPriorInAllList()
{
var priorIndexAllChildren = 0;
var newIndex = 0;
var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex);
Assert.Equal(0, adjustedIndex);
}
[Fact]
public void DetermineAdjustedIndexPriorInMiddleOfAllList()
{
var priorIndexAllChildren = 2;
var newIndex = 0;
var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex);
Assert.Equal(1, adjustedIndex);
}
[Fact]
public void DetermineAdjustedIndexMoveMiddleOfPlaylist()
{
var priorIndexAllChildren = 2;
var newIndex = 1;
var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex);
Assert.Equal(3, adjustedIndex);
}
}
@@ -0,0 +1,339 @@
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using AutoFixture;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.Plugins;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Plugins
{
public class PluginManagerTests
{
private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
private string _tempPath = string.Empty;
private string _pluginPath = string.Empty;
private JsonSerializerOptions _options;
public PluginManagerTests()
{
(_tempPath, _pluginPath) = GetTestPaths("plugin-" + Path.GetRandomFileName());
Directory.CreateDirectory(_pluginPath);
_options = GetTestSerializerOptions();
}
[Fact]
public void SaveManifest_RoundTrip_Success()
{
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, null!, new Version(1, 0));
var manifest = new PluginManifest()
{
Version = "1.0"
};
Assert.True(pluginManager.SaveManifest(manifest, _pluginPath));
var res = pluginManager.LoadManifest(_pluginPath);
Assert.Equal(manifest.Category, res.Manifest.Category);
Assert.Equal(manifest.Changelog, res.Manifest.Changelog);
Assert.Equal(manifest.Description, res.Manifest.Description);
Assert.Equal(manifest.Id, res.Manifest.Id);
Assert.Equal(manifest.Name, res.Manifest.Name);
Assert.Equal(manifest.Overview, res.Manifest.Overview);
Assert.Equal(manifest.Owner, res.Manifest.Owner);
Assert.Equal(manifest.TargetAbi, res.Manifest.TargetAbi);
Assert.Equal(manifest.Timestamp, res.Manifest.Timestamp);
Assert.Equal(manifest.Version, res.Manifest.Version);
Assert.Equal(manifest.Status, res.Manifest.Status);
Assert.Equal(manifest.AutoUpdate, res.Manifest.AutoUpdate);
Assert.Equal(manifest.ImagePath, res.Manifest.ImagePath);
Assert.Equal(manifest.Assemblies, res.Manifest.Assemblies);
}
/// <summary>
/// Tests safe traversal within the plugin directory.
/// </summary>
/// <param name="dllFile">The safe path to evaluate.</param>
[Theory]
[InlineData("./some.dll")]
[InlineData("some.dll")]
[InlineData("sub/path/some.dll")]
public void Constructor_DiscoversSafePluginAssembly_Status_Active(string dllFile)
{
var manifest = new PluginManifest
{
Id = Guid.NewGuid(),
Name = "Safe Assembly",
Assemblies = new string[] { dllFile }
};
var filename = Path.GetFileName(dllFile)!;
var dllPath = Path.GetDirectoryName(Path.Combine(_pluginPath, dllFile))!;
Directory.CreateDirectory(dllPath);
FileHelper.CreateEmpty(Path.Combine(dllPath, filename));
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options);
var expectedFullPath = Path.Combine(_pluginPath, dllFile).Canonicalize();
Assert.NotNull(res);
Assert.NotEmpty(pluginManager.Plugins);
Assert.Equal(PluginStatus.Active, res!.Status);
Assert.Equal(expectedFullPath, pluginManager.Plugins[0].DllFiles[0]);
Assert.StartsWith(_pluginPath, expectedFullPath, StringComparison.InvariantCulture);
}
/// <summary>
/// Tests unsafe attempts to traverse to higher directories.
/// </summary>
/// <remarks>
/// Attempts to load directories outside of the plugin should be
/// constrained. Path traversal, shell expansion, and double encoding
/// can be used to load unintended files.
/// See <see href="https://owasp.org/www-community/attacks/Path_Traversal"/> for more.
/// </remarks>
/// <param name="unsafePath">The unsafe path to evaluate.</param>
[Theory]
[InlineData("/some.dll")] // Root path.
[InlineData("../some.dll")] // Simple traversal.
[InlineData("C:\\some.dll")] // Windows root path.
[InlineData("test.txt")] // Not a DLL
[InlineData(".././.././../some.dll")] // Traversal with current and parent
[InlineData(@"..\.\..\.\..\some.dll")] // Windows traversal with current and parent
[InlineData(@"\\network\resource.dll")] // UNC Path
[InlineData("https://jellyfin.org/some.dll")] // URL
[InlineData("~/some.dll")] // Tilde poses a shell expansion risk, but is a valid path character.
public void Constructor_DiscoversUnsafePluginAssembly_Status_Malfunctioned(string unsafePath)
{
var manifest = new PluginManifest
{
Id = Guid.NewGuid(),
Name = "Unsafe Assembly",
Assemblies = new string[] { unsafePath }
};
// Only create very specific files. Otherwise the test will be exploiting path traversal.
var files = new string[]
{
"../other.dll",
"some.dll"
};
foreach (var file in files)
{
FileHelper.CreateEmpty(Path.Combine(_pluginPath, file));
}
var metafilePath = Path.Combine(_pluginPath, "meta.json");
File.WriteAllText(metafilePath, JsonSerializer.Serialize(manifest, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
var res = JsonSerializer.Deserialize<PluginManifest>(File.ReadAllText(metafilePath), _options);
Assert.NotNull(res);
Assert.Empty(pluginManager.Plugins);
Assert.Equal(PluginStatus.Malfunctioned, res!.Status);
}
[Fact]
public async Task PopulateManifest_ExistingMetafilePlugin_PopulatesMissingFields()
{
var packageInfo = GenerateTestPackage();
// Partial plugin without a name, but matching version and package ID
var partial = new PluginManifest
{
Id = packageInfo.Id,
AutoUpdate = false, // Turn off AutoUpdate
Status = PluginStatus.Restart,
Version = new Version(1, 0, 0).ToString(),
Assemblies = new[] { "Jellyfin.Test.dll" }
};
var expectedManifest = new PluginManifest
{
Id = partial.Id,
Name = packageInfo.Name,
AutoUpdate = partial.AutoUpdate,
Status = PluginStatus.Active,
Owner = packageInfo.Owner,
Assemblies = partial.Assemblies,
Category = packageInfo.Category,
Description = packageInfo.Description,
Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!,
Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime,
Changelog = packageInfo.Versions[0].Changelog!,
Version = new Version(1, 0).ToString(),
ImagePath = string.Empty
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equivalent(expectedManifest, result);
}
[Fact]
public async Task PopulateManifest_NoMetafile_PreservesManifest()
{
var packageInfo = GenerateTestPackage();
var expectedManifest = new PluginManifest
{
Id = packageInfo.Id,
Name = packageInfo.Name,
AutoUpdate = true,
Status = PluginStatus.Active,
Owner = packageInfo.Owner,
Assemblies = Array.Empty<string>(),
Category = packageInfo.Category,
Description = packageInfo.Description,
Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!,
Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime,
Changelog = packageInfo.Versions[0].Changelog!,
Version = packageInfo.Versions[0].Version,
ImagePath = string.Empty
};
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, null!, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var metafilePath = Path.Combine(_pluginPath, "meta.json");
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equivalent(expectedManifest, result);
}
[Fact]
public async Task PopulateManifest_ExistingMetafileMismatchedIds_Status_Malfunctioned()
{
var packageInfo = GenerateTestPackage();
// Partial plugin without a name, but matching version and package ID
var partial = new PluginManifest
{
Id = Guid.NewGuid(),
Version = new Version(1, 0, 0).ToString()
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equal(packageInfo.Name, result.Name);
Assert.Equal(PluginStatus.Malfunctioned, result.Status);
}
[Fact]
public async Task PopulateManifest_ExistingMetafileMismatchedVersions_Updates_Version()
{
var packageInfo = GenerateTestPackage();
var partial = new PluginManifest
{
Id = packageInfo.Id,
Version = new Version(2, 0, 0).ToString()
};
var metafilePath = Path.Combine(_pluginPath, "meta.json");
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
Assert.NotNull(result);
Assert.Equal(packageInfo.Name, result.Name);
Assert.Equal(PluginStatus.Active, result.Status);
Assert.Equal(packageInfo.Versions[0].Version, result.Version);
}
private PackageInfo GenerateTestPackage()
{
var fixture = new Fixture();
fixture.Customize<PackageInfo>(c => c.Without(x => x.Versions).Without(x => x.ImageUrl));
fixture.Customize<VersionInfo>(c => c.Without(x => x.Version).Without(x => x.Timestamp));
var versionInfo = fixture.Create<VersionInfo>();
versionInfo.Version = new Version(1, 0).ToString();
versionInfo.Timestamp = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);
var packageInfo = fixture.Create<PackageInfo>();
packageInfo.Versions = new[] { versionInfo };
return packageInfo;
}
private JsonSerializerOptions GetTestSerializerOptions()
{
var options = new JsonSerializerOptions(JsonDefaults.Options)
{
WriteIndented = true
};
for (var i = 0; i < options.Converters.Count; i++)
{
// Remove the Guid converter for parity with plugin manager.
if (options.Converters[i] is JsonGuidConverter converter)
{
options.Converters.Remove(converter);
}
}
return options;
}
private (string TempPath, string PluginPath) GetTestPaths(string pluginFolderName)
{
var tempPath = Path.Combine(_testPathRoot, "plugin-manager" + Path.GetRandomFileName());
var pluginPath = Path.Combine(tempPath, pluginFolderName);
return (tempPath, pluginPath);
}
}
}
@@ -0,0 +1,139 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.QuickConnect;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.QuickConnect
{
public class QuickConnectManagerTests
{
private static readonly AuthorizationInfo _quickConnectAuthInfo = new AuthorizationInfo
{
Device = "Device",
DeviceId = "DeviceId",
Client = "Client",
Version = "1.0.0"
};
private readonly Fixture _fixture;
private readonly ServerConfiguration _config;
private readonly QuickConnectManager _quickConnectManager;
public QuickConnectManagerTests()
{
_config = new ServerConfiguration();
var configManager = new Mock<IServerConfigurationManager>();
configManager.Setup(x => x.Configuration).Returns(_config);
_fixture = new Fixture();
_fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true
}).Inject(configManager.Object);
// User object contains circular references.
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_quickConnectManager = _fixture.Create<QuickConnectManager>();
}
[Fact]
public void IsEnabled_QuickConnectUnavailable_False()
{
_config.QuickConnectAvailable = false;
Assert.False(_quickConnectManager.IsEnabled);
}
[Theory]
[InlineData("", "DeviceId", "Client", "1.0.0")]
[InlineData("Device", "", "Client", "1.0.0")]
[InlineData("Device", "DeviceId", "", "1.0.0")]
[InlineData("Device", "DeviceId", "Client", "")]
public void TryConnect_InvalidAuthorizationInfo_ThrowsArgumentException(string device, string deviceId, string client, string version)
=> Assert.Throws<ArgumentException>(() => _quickConnectManager.TryConnect(
new AuthorizationInfo
{
Device = device,
DeviceId = deviceId,
Client = client,
Version = version
}));
[Fact]
public void TryConnect_QuickConnectUnavailable_ThrowsAuthenticationException()
{
_config.QuickConnectAvailable = false;
Assert.Throws<AuthenticationException>(() => _quickConnectManager.TryConnect(_quickConnectAuthInfo));
}
[Fact]
public void CheckRequestStatus_QuickConnectUnavailable_ThrowsAuthenticationException()
{
_config.QuickConnectAvailable = false;
Assert.Throws<AuthenticationException>(() => _quickConnectManager.CheckRequestStatus(string.Empty));
}
[Fact]
public async Task AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
{
_config.QuickConnectAvailable = false;
await Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty));
}
[Fact]
public void GetAuthorizedRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
{
_config.QuickConnectAvailable = false;
Assert.Throws<AuthenticationException>(() => _quickConnectManager.GetAuthorizedRequest(string.Empty));
}
[Fact]
public void IsEnabled_QuickConnectAvailable_True()
{
_config.QuickConnectAvailable = true;
Assert.True(_quickConnectManager.IsEnabled);
}
[Fact]
public void CheckRequestStatus_QuickConnectAvailable_Success()
{
_config.QuickConnectAvailable = true;
var res1 = _quickConnectManager.TryConnect(_quickConnectAuthInfo);
var res2 = _quickConnectManager.CheckRequestStatus(res1.Secret);
Assert.Equal(res1, res2);
}
[Fact]
public void CheckRequestStatus_UnknownSecret_ThrowsResourceNotFoundException()
{
_config.QuickConnectAvailable = true;
Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.CheckRequestStatus("Unknown secret"));
}
[Fact]
public void GetAuthorizedRequest_UnknownSecret_ThrowsResourceNotFoundException()
{
_config.QuickConnectAvailable = true;
Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.GetAuthorizedRequest("Unknown secret"));
}
[Fact]
public async Task AuthorizeRequest_QuickConnectAvailable_Success()
{
_config.QuickConnectAvailable = true;
var res = _quickConnectManager.TryConnect(_quickConnectAuthInfo);
Assert.True(await _quickConnectManager.AuthorizeRequest(Guid.Empty, res.Code));
}
}
}
@@ -0,0 +1,111 @@
using System;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.SessionManager;
public class SessionManagerTests
{
[Theory]
[InlineData("", typeof(ArgumentException))]
[InlineData(null, typeof(ArgumentNullException))]
public async Task GetAuthorizationToken_Should_ThrowException(string? deviceId, Type exceptionType)
{
await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
Mock.Of<IEventManager>(),
Mock.Of<IUserDataManager>(),
Mock.Of<IServerConfigurationManager>(),
Mock.Of<ILibraryManager>(),
Mock.Of<IUserManager>(),
Mock.Of<IMusicManager>(),
Mock.Of<IDtoService>(),
Mock.Of<IImageProcessor>(),
Mock.Of<IServerApplicationHost>(),
Mock.Of<IDeviceManager>(),
Mock.Of<IMediaSourceManager>(),
Mock.Of<IHostApplicationLifetime>());
await Assert.ThrowsAsync(exceptionType, () => sessionManager.GetAuthorizationToken(
new User("test", "default", "default"),
deviceId,
"app_name",
"0.0.0",
"device_name"));
}
[Theory]
[MemberData(nameof(AuthenticateNewSessionInternal_Exception_TestData))]
public async Task AuthenticateNewSessionInternal_Should_ThrowException(AuthenticationRequest authenticationRequest, Type exceptionType)
{
await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
Mock.Of<IEventManager>(),
Mock.Of<IUserDataManager>(),
Mock.Of<IServerConfigurationManager>(),
Mock.Of<ILibraryManager>(),
Mock.Of<IUserManager>(),
Mock.Of<IMusicManager>(),
Mock.Of<IDtoService>(),
Mock.Of<IImageProcessor>(),
Mock.Of<IServerApplicationHost>(),
Mock.Of<IDeviceManager>(),
Mock.Of<IMediaSourceManager>(),
Mock.Of<IHostApplicationLifetime>());
await Assert.ThrowsAsync(exceptionType, () => sessionManager.AuthenticateNewSessionInternal(authenticationRequest, false));
}
public static TheoryData<AuthenticationRequest, Type> AuthenticateNewSessionInternal_Exception_TestData()
{
var data = new TheoryData<AuthenticationRequest, Type>
{
{
new AuthenticationRequest { App = string.Empty, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" },
typeof(ArgumentException)
},
{
new AuthenticationRequest { App = null, DeviceId = "device_id", DeviceName = "device_name", AppVersion = "app_version" },
typeof(ArgumentNullException)
},
{
new AuthenticationRequest { App = "app_name", DeviceId = string.Empty, DeviceName = "device_name", AppVersion = "app_version" },
typeof(ArgumentException)
},
{
new AuthenticationRequest { App = "app_name", DeviceId = null, DeviceName = "device_name", AppVersion = "app_version" },
typeof(ArgumentNullException)
},
{
new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = string.Empty, AppVersion = "app_version" },
typeof(ArgumentException)
},
{
new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = null, AppVersion = "app_version" },
typeof(ArgumentNullException)
},
{
new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = string.Empty },
typeof(ArgumentException)
},
{
new AuthenticationRequest { App = "app_name", DeviceId = "device_id", DeviceName = "device_name", AppVersion = null },
typeof(ArgumentNullException)
}
};
return data;
}
}
@@ -0,0 +1,163 @@
using System;
using Emby.Server.Implementations.Sorting;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Sorting
{
public class AiredEpisodeOrderComparerTests
{
private readonly AiredEpisodeOrderComparer _cmp = new AiredEpisodeOrderComparer();
[Theory]
[ClassData(typeof(EpisodeBadData))]
public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
{
Assert.Throws<ArgumentNullException>(() => _cmp.Compare(x, y));
}
[Theory]
[ClassData(typeof(EpisodeTestData))]
public void AiredEpisodeOrderCompareTest(BaseItem x, BaseItem y, int expected)
{
Assert.Equal(expected, _cmp.Compare(x, y));
Assert.Equal(-expected, _cmp.Compare(y, x));
}
private sealed class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
{
public EpisodeBadData()
{
Add(null, new Episode());
Add(new Episode(), null);
}
}
private sealed class EpisodeTestData : TheoryData<BaseItem, BaseItem, int>
{
public EpisodeTestData()
{
Add(
new Movie(),
new Movie(),
0);
Add(
new Movie(),
new Episode(),
1);
// Good cases
Add(
new Episode(),
new Episode(),
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 2, IndexNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
1);
// Good Specials
Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
0);
Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1);
// Specials to Episodes
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
1);
Add(
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1 },
1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1);
Add(
new Episode { ParentIndexNumber = 1 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 3 },
new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
1);
// Premiere Date
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
0);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
-1);
Add(
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
1);
}
}
}
}
@@ -0,0 +1,49 @@
using System;
using Emby.Server.Implementations.Sorting;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Sorting;
public class IndexNumberComparerTests
{
private readonly IndexNumberComparer _cmp = new IndexNumberComparer();
public static TheoryData<BaseItem?, BaseItem?> Compare_GivenNull_ThrowsArgumentNullException_TestData()
=> new()
{
{ null, new Audio() },
{ new Audio(), null }
};
[Theory]
[MemberData(nameof(Compare_GivenNull_ThrowsArgumentNullException_TestData))]
public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
{
Assert.Throws<ArgumentNullException>(() => _cmp.Compare(x, y));
}
[Theory]
[InlineData(null, null, 0)]
[InlineData(0, null, 1)]
[InlineData(null, 0, -1)]
[InlineData(1, 1, 0)]
[InlineData(0, 1, -1)]
[InlineData(1, 0, 1)]
public void Compare_ValidIndices_SortsExpected(int? index1, int? index2, int expected)
{
BaseItem x = new Audio
{
IndexNumber = index1
};
BaseItem y = new Audio
{
IndexNumber = index2
};
Assert.Equal(expected, _cmp.Compare(x, y));
Assert.Equal(-expected, _cmp.Compare(y, x));
}
}
@@ -0,0 +1,49 @@
using System;
using Emby.Server.Implementations.Sorting;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Sorting;
public class ParentIndexNumberComparerTests
{
private readonly ParentIndexNumberComparer _cmp = new ParentIndexNumberComparer();
public static TheoryData<BaseItem?, BaseItem?> Compare_GivenNull_ThrowsArgumentNullException_TestData()
=> new()
{
{ null, new Audio() },
{ new Audio(), null }
};
[Theory]
[MemberData(nameof(Compare_GivenNull_ThrowsArgumentNullException_TestData))]
public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
{
Assert.Throws<ArgumentNullException>(() => _cmp.Compare(x, y));
}
[Theory]
[InlineData(null, null, 0)]
[InlineData(0, null, 1)]
[InlineData(null, 0, -1)]
[InlineData(1, 1, 0)]
[InlineData(0, 1, -1)]
[InlineData(1, 0, 1)]
public void Compare_ValidIndices_SortsExpected(int? parentIndex1, int? parentIndex2, int expected)
{
BaseItem x = new Audio
{
ParentIndexNumber = parentIndex1
};
BaseItem y = new Audio
{
ParentIndexNumber = parentIndex2
};
Assert.Equal(expected, _cmp.Compare(x, y));
Assert.Equal(-expected, _cmp.Compare(y, x));
}
}
@@ -0,0 +1,76 @@
using System;
using Emby.Server.Implementations.Sorting;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Sorting
{
public class PremiereDateComparerTests
{
private readonly PremiereDateComparer _cmp = new PremiereDateComparer();
[Theory]
[ClassData(typeof(PremiereDateTestData))]
public void PremiereDateCompareTest(BaseItem x, BaseItem y, int expected)
{
Assert.Equal(expected, _cmp.Compare(x, y));
Assert.Equal(-expected, _cmp.Compare(y, x));
}
private sealed class PremiereDateTestData : TheoryData<BaseItem, BaseItem, int>
{
public PremiereDateTestData()
{
// Happy case - Both have premier date
// Expected: x listed first
Add(
new Movie { PremiereDate = new DateTime(2018, 1, 1) },
new Movie { PremiereDate = new DateTime(2018, 1, 3) },
-1);
// Both have premiere date, but y has invalid date
// Expected: y listed first
Add(
new Movie { PremiereDate = new DateTime(2019, 1, 1) },
new Movie { PremiereDate = new DateTime(03, 1, 1) },
1);
// Only x has premiere date, with earlier year than y
// Expected: x listed first
Add(
new Movie { PremiereDate = new DateTime(2020, 1, 1) },
new Movie { ProductionYear = 2021 },
-1);
// Only x has premiere date, with same year as y
// Expected: y listed first
Add(
new Movie { PremiereDate = new DateTime(2022, 1, 2) },
new Movie { ProductionYear = 2022 },
1);
// Only x has a premiere date, with later year than y
// Expected: y listed first
Add(
new Movie { PremiereDate = new DateTime(2024, 3, 1) },
new Movie { ProductionYear = 2023 },
1);
// Only x has a premiere date, y has an invalid year
// Expected: y listed first
Add(
new Movie { PremiereDate = new DateTime(2025, 1, 1) },
new Movie { ProductionYear = 0 },
1);
// Only x has a premiere date, y has neither date nor year
// Expected: y listed first
Add(
new Movie { PremiereDate = new DateTime(2026, 1, 1) },
new Movie(),
1);
}
}
}
}
@@ -0,0 +1 @@
{"MessageType":"ForceKeepAlive","MessageId":"00000000-0000-0000-0000-000000000000","ServerId":null,"Data":60}
@@ -0,0 +1 @@
{"MessageType":"KeepAlive","MessageId":"d29ef449-6965-4000
@@ -0,0 +1 @@
{"MessageType":"ForceKeepAlive","MessageId":"00000000-0000-0000-0000-000000000000","ServerId":null,"Data":60}{"MessageType":"KeepAlive","MessageId":"d29ef449-6965-4000
@@ -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 compatible 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 into 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"
}
]
}
]
@@ -0,0 +1,63 @@
using System;
using System.Linq;
using Jellyfin.Data.Enums;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.TypedBaseItem
{
public class BaseItemKindTests
{
public static TheoryData<Type> BaseItemKind_TestData()
{
var data = new TheoryData<Type>();
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in loadedAssemblies)
{
if (IsProjectAssemblyName(assembly.FullName))
{
var baseItemTypes = assembly.GetTypes()
.Where(targetType => targetType.IsClass
&& !targetType.IsAbstract
&& targetType.IsSubclassOf(typeof(MediaBrowser.Controller.Entities.BaseItem)));
foreach (var baseItemType in baseItemTypes)
{
data.Add(baseItemType);
}
}
}
return data;
}
[Theory]
[MemberData(nameof(BaseItemKind_TestData))]
public void EnumParse_GivenValidBaseItemType_ReturnsEnumValue(Type baseItemDescendantType)
{
var enumValue = Enum.Parse<BaseItemKind>(baseItemDescendantType.Name);
Assert.True(Enum.IsDefined(enumValue));
}
[Theory]
[MemberData(nameof(BaseItemKind_TestData))]
public void GetBaseItemKind_WhenCalledAfterDefaultCtor_DoesNotThrow(Type baseItemDescendantType)
{
var defaultConstructor = baseItemDescendantType.GetConstructor(Type.EmptyTypes);
var instance = (MediaBrowser.Controller.Entities.BaseItem)defaultConstructor!.Invoke(null);
var exception = Record.Exception(() => instance.GetBaseItemKind());
Assert.Null(exception);
}
private static bool IsProjectAssemblyName(string? name)
{
if (name is null)
{
return false;
}
return name.StartsWith("Jellyfin", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("Emby", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("MediaBrowser", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,110 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.Updates;
using MediaBrowser.Model.Updates;
using Moq;
using Moq.Protected;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Updates
{
public class InstallationManagerTests
{
private readonly Fixture _fixture;
private readonly InstallationManager _installationManager;
public InstallationManagerTests()
{
var messageHandler = new Mock<HttpMessageHandler>();
messageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Returns<HttpRequestMessage, CancellationToken>(
(m, _) =>
{
return Task.FromResult(new HttpResponseMessage()
{
Content = new StreamContent(File.OpenRead("Test Data/Updates/" + m.RequestUri?.Segments[^1]))
});
});
var http = new Mock<IHttpClientFactory>();
http.Setup(x => x.CreateClient(It.IsAny<string>()))
.Returns(new HttpClient(messageHandler.Object));
_fixture = new Fixture();
_fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true
});
_fixture.Inject(http);
_installationManager = _fixture.Create<InstallationManager>();
}
[Fact]
public async Task GetPackages_Valid_Success()
{
PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/files/plugin/manifest.json",
false);
Assert.Equal(25, packages.Length);
}
[Fact]
public async Task FilterPackages_NameOnly_Success()
{
PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/files/plugin/manifest.json",
false);
packages = _installationManager.FilterPackages(packages, "Anime").ToArray();
Assert.Single(packages);
}
[Fact]
public async Task FilterPackages_GuidOnly_Success()
{
PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/files/plugin/manifest.json",
false);
packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
Assert.Single(packages);
}
[Fact]
public async Task InstallPackage_InvalidChecksum_ThrowsInvalidDataException()
{
var packageInfo = new InstallationInfo()
{
Name = "Test",
SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
Checksum = "InvalidChecksum"
};
await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None));
}
[Fact]
public async Task InstallPackage_Valid_Success()
{
var packageInfo = new InstallationInfo()
{
Name = "Test",
SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
Checksum = "11b5b2f1a9ebc4f66d6ef19018543361"
};
var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None));
Assert.Null(ex);
}
}
}
@@ -0,0 +1,35 @@
using System;
using Jellyfin.Server.Implementations.Users;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Users
{
public class UserManagerTests
{
[Theory]
[InlineData("this_is_valid")]
[InlineData("this is also valid")]
[InlineData("0@_-' .")]
[InlineData("Aa0@_-' .+")]
[InlineData("thisisa+testemail@test.foo")]
[InlineData("------@@@--+++----@@--abcdefghijklmn---------@----_-_-___-_ .9foo+")]
public void ThrowIfInvalidUsername_WhenValidUsername_DoesNotThrowArgumentException(string username)
{
var ex = Record.Exception(() => UserManager.ThrowIfInvalidUsername(username));
Assert.Null(ex);
}
[Theory]
[InlineData(" ")]
[InlineData("")]
[InlineData("special characters like & $ ? are not allowed")]
[InlineData("thishasaspaceontheend ")]
[InlineData(" thishasaspaceatthestart")]
[InlineData(" thishasaspaceatbothends ")]
[InlineData(" this has a space at both ends and inbetween ")]
public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username)
{
Assert.Throws<ArgumentException>(() => UserManager.ThrowIfInvalidUsername(username));
}
}
}
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.Server.Implementations.Tests")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.Server.Implementations.Tests")]
[assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.Server.Implementations.Tests")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.
@@ -0,0 +1 @@
cb21fbd6437988fcfb787098d7235ff23802059a71376dce54aff99ad1f79cb9
@@ -0,0 +1,27 @@
is_global = true
build_property.TargetFramework = net10.0
build_property.TargetFramework = net10.0
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkVersion = v10.0
build_property.TargetFrameworkVersion = v10.0
build_property.TargetPlatformMinVersion =
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Jellyfin.Server.Implementations.Tests
build_property.ProjectDir = /srv/common_drive/Projects/pgsql-jellyfin/tests/Jellyfin.Server.Implementations.Tests/
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0
build_property.EnableCodeStyleSeverity =
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/home/wjones/.nuget/packages/" />
</ItemGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)xunit.runner.visualstudio/2.8.2/build/net6.0/xunit.runner.visualstudio.props" Condition="Exists('$(NuGetPackageRoot)xunit.runner.visualstudio/2.8.2/build/net6.0/xunit.runner.visualstudio.props')" />
<Import Project="$(NuGetPackageRoot)xunit.core/2.9.3/build/xunit.core.props" Condition="Exists('$(NuGetPackageRoot)xunit.core/2.9.3/build/xunit.core.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.testplatform.testhost/18.0.1/build/net8.0/Microsoft.TestPlatform.TestHost.props" Condition="Exists('$(NuGetPackageRoot)microsoft.testplatform.testhost/18.0.1/build/net8.0/Microsoft.TestPlatform.TestHost.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.codecoverage/18.0.1/build/netstandard2.0/Microsoft.CodeCoverage.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codecoverage/18.0.1/build/netstandard2.0/Microsoft.CodeCoverage.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.test.sdk/18.0.1/build/net8.0/Microsoft.NET.Test.Sdk.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.test.sdk/18.0.1/build/net8.0/Microsoft.NET.Test.Sdk.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Configuration.UserSecrets.props" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Configuration.UserSecrets.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props')" />
</ImportGroup>
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Pkgxunit_analyzers Condition=" '$(Pkgxunit_analyzers)' == '' ">/home/wjones/.nuget/packages/xunit.analyzers/1.18.0</Pkgxunit_analyzers>
<PkgMicrosoft_Extensions_ApiDescription_Server Condition=" '$(PkgMicrosoft_Extensions_ApiDescription_Server)' == '' ">/home/wjones/.nuget/packages/microsoft.extensions.apidescription.server/6.0.5</PkgMicrosoft_Extensions_ApiDescription_Server>
<PkgStyleCop_Analyzers_Unstable Condition=" '$(PkgStyleCop_Analyzers_Unstable)' == '' ">/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556</PkgStyleCop_Analyzers_Unstable>
<PkgSmartAnalyzers_MultithreadingAnalyzer Condition=" '$(PkgSmartAnalyzers_MultithreadingAnalyzer)' == '' ">/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31</PkgSmartAnalyzers_MultithreadingAnalyzer>
<PkgSerilogAnalyzer Condition=" '$(PkgSerilogAnalyzer)' == '' ">/home/wjones/.nuget/packages/seriloganalyzer/0.15.0</PkgSerilogAnalyzer>
<PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers Condition=" '$(PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers)' == '' ">/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0</PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers>
</PropertyGroup>
</Project>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)xunit.core/2.9.3/build/xunit.core.targets" Condition="Exists('$(NuGetPackageRoot)xunit.core/2.9.3/build/xunit.core.targets')" />
<Import Project="$(NuGetPackageRoot)sqlitepclraw.lib.e_sqlite3/2.1.11/buildTransitive/net9.0/SQLitePCLRaw.lib.e_sqlite3.targets" Condition="Exists('$(NuGetPackageRoot)sqlitepclraw.lib.e_sqlite3/2.1.11/buildTransitive/net9.0/SQLitePCLRaw.lib.e_sqlite3.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder/10.0.3/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder/10.0.3/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Options.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.testplatform.testhost/18.0.1/build/net8.0/Microsoft.TestPlatform.TestHost.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.testplatform.testhost/18.0.1/build/net8.0/Microsoft.TestPlatform.TestHost.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.codecoverage/18.0.1/build/netstandard2.0/Microsoft.CodeCoverage.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codecoverage/18.0.1/build/netstandard2.0/Microsoft.CodeCoverage.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.test.sdk/18.0.1/build/net8.0/Microsoft.NET.Test.Sdk.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.test.sdk/18.0.1/build/net8.0/Microsoft.NET.Test.Sdk.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Configuration.UserSecrets.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Configuration.UserSecrets.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.mvc.testing/10.0.3/buildTransitive/net10.0/Microsoft.AspNetCore.Mvc.Testing.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.mvc.testing/10.0.3/buildTransitive/net10.0/Microsoft.AspNetCore.Mvc.Testing.targets')" />
<Import Project="$(NuGetPackageRoot)coverlet.collector/8.0.0/build/net8.0/coverlet.collector.targets" Condition="Exists('$(NuGetPackageRoot)coverlet.collector/8.0.0/build/net8.0/coverlet.collector.targets')" />
</ImportGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,163 @@
{
"version": 2,
"dgSpecHash": "di8TdLMk6N0=",
"success": true,
"projectFilePath": "/srv/common_drive/Projects/pgsql-jellyfin/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj",
"expectedPackageFiles": [
"/home/wjones/.nuget/packages/asynckeyedlock/8.0.2/asynckeyedlock.8.0.2.nupkg.sha512",
"/home/wjones/.nuget/packages/autofixture/4.18.1/autofixture.4.18.1.nupkg.sha512",
"/home/wjones/.nuget/packages/autofixture.automoq/4.18.1/autofixture.automoq.4.18.1.nupkg.sha512",
"/home/wjones/.nuget/packages/autofixture.xunit2/4.18.1/autofixture.xunit2.4.18.1.nupkg.sha512",
"/home/wjones/.nuget/packages/bdinfo/0.8.0/bdinfo.0.8.0.nupkg.sha512",
"/home/wjones/.nuget/packages/bitfaster.caching/2.5.4/bitfaster.caching.2.5.4.nupkg.sha512",
"/home/wjones/.nuget/packages/blurhashsharp/1.4.0-pre.1/blurhashsharp.1.4.0-pre.1.nupkg.sha512",
"/home/wjones/.nuget/packages/blurhashsharp.skiasharp/1.4.0-pre.1/blurhashsharp.skiasharp.1.4.0-pre.1.nupkg.sha512",
"/home/wjones/.nuget/packages/castle.core/5.1.1/castle.core.5.1.1.nupkg.sha512",
"/home/wjones/.nuget/packages/commandlineparser/2.9.1/commandlineparser.2.9.1.nupkg.sha512",
"/home/wjones/.nuget/packages/coverlet.collector/8.0.0/coverlet.collector.8.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/diacritics/4.1.4/diacritics.4.1.4.nupkg.sha512",
"/home/wjones/.nuget/packages/discutils.core/0.16.13/discutils.core.0.16.13.nupkg.sha512",
"/home/wjones/.nuget/packages/discutils.iso9660/0.16.13/discutils.iso9660.0.16.13.nupkg.sha512",
"/home/wjones/.nuget/packages/discutils.streams/0.16.13/discutils.streams.0.16.13.nupkg.sha512",
"/home/wjones/.nuget/packages/discutils.udf/0.16.13/discutils.udf.0.16.13.nupkg.sha512",
"/home/wjones/.nuget/packages/dotnet.glob/3.1.3/dotnet.glob.3.1.3.nupkg.sha512",
"/home/wjones/.nuget/packages/excss/4.3.1/excss.4.3.1.nupkg.sha512",
"/home/wjones/.nuget/packages/fare/2.1.1/fare.2.1.1.nupkg.sha512",
"/home/wjones/.nuget/packages/harfbuzzsharp/8.3.0.1/harfbuzzsharp.8.3.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/harfbuzzsharp.nativeassets.linux/8.3.1.1/harfbuzzsharp.nativeassets.linux.8.3.1.1.nupkg.sha512",
"/home/wjones/.nuget/packages/harfbuzzsharp.nativeassets.macos/8.3.0.1/harfbuzzsharp.nativeassets.macos.8.3.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/harfbuzzsharp.nativeassets.win32/8.3.0.1/harfbuzzsharp.nativeassets.win32.8.3.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/icu4n/60.1.0-alpha.356/icu4n.60.1.0-alpha.356.nupkg.sha512",
"/home/wjones/.nuget/packages/icu4n.transliterator/60.1.0-alpha.356/icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512",
"/home/wjones/.nuget/packages/ignore/0.2.1/ignore.0.2.1.nupkg.sha512",
"/home/wjones/.nuget/packages/j2n/2.0.0/j2n.2.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/jellyfin.xmltv/10.8.0/jellyfin.xmltv.10.8.0.nupkg.sha512",
"/home/wjones/.nuget/packages/libse/4.0.12/libse.4.0.12.nupkg.sha512",
"/home/wjones/.nuget/packages/lrcparser/2025.623.0/lrcparser.2025.623.0.nupkg.sha512",
"/home/wjones/.nuget/packages/metabrainz.common/4.1.1/metabrainz.common.4.1.1.nupkg.sha512",
"/home/wjones/.nuget/packages/metabrainz.common.json/7.2.0/metabrainz.common.json.7.2.0.nupkg.sha512",
"/home/wjones/.nuget/packages/metabrainz.musicbrainz/8.0.1/metabrainz.musicbrainz.8.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.aspnetcore.authorization/10.0.3/microsoft.aspnetcore.authorization.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.aspnetcore.metadata/10.0.3/microsoft.aspnetcore.metadata.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.aspnetcore.mvc.testing/10.0.3/microsoft.aspnetcore.mvc.testing.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.aspnetcore.testhost/10.0.3/microsoft.aspnetcore.testhost.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0/microsoft.codeanalysis.bannedapianalyzers.4.14.0.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.codecoverage/18.0.1/microsoft.codecoverage.18.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.data.sqlite/10.0.3/microsoft.data.sqlite.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.data.sqlite.core/10.0.3/microsoft.data.sqlite.core.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore/10.0.3/microsoft.entityframeworkcore.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.abstractions/10.0.3/microsoft.entityframeworkcore.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.analyzers/10.0.3/microsoft.entityframeworkcore.analyzers.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.relational/10.0.3/microsoft.entityframeworkcore.relational.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.sqlite/10.0.3/microsoft.entityframeworkcore.sqlite.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.sqlite.core/10.0.3/microsoft.entityframeworkcore.sqlite.core.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.apidescription.server/6.0.5/microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.caching.abstractions/10.0.3/microsoft.extensions.caching.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.caching.memory/10.0.3/microsoft.extensions.caching.memory.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration/10.0.3/microsoft.extensions.configuration.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.abstractions/10.0.3/microsoft.extensions.configuration.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.binder/10.0.3/microsoft.extensions.configuration.binder.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.commandline/10.0.3/microsoft.extensions.configuration.commandline.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.environmentvariables/10.0.3/microsoft.extensions.configuration.environmentvariables.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.fileextensions/10.0.3/microsoft.extensions.configuration.fileextensions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.json/10.0.3/microsoft.extensions.configuration.json.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.usersecrets/10.0.3/microsoft.extensions.configuration.usersecrets.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.dependencyinjection/10.0.3/microsoft.extensions.dependencyinjection.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/10.0.3/microsoft.extensions.dependencyinjection.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.dependencymodel/10.0.3/microsoft.extensions.dependencymodel.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics/10.0.3/microsoft.extensions.diagnostics.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics.abstractions/10.0.3/microsoft.extensions.diagnostics.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics.healthchecks/10.0.3/microsoft.extensions.diagnostics.healthchecks.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics.healthchecks.abstractions/10.0.3/microsoft.extensions.diagnostics.healthchecks.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics.healthchecks.entityframeworkcore/10.0.3/microsoft.extensions.diagnostics.healthchecks.entityframeworkcore.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.fileproviders.abstractions/10.0.3/microsoft.extensions.fileproviders.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.fileproviders.physical/10.0.3/microsoft.extensions.fileproviders.physical.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.filesystemglobbing/10.0.3/microsoft.extensions.filesystemglobbing.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.hosting/10.0.3/microsoft.extensions.hosting.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.hosting.abstractions/10.0.3/microsoft.extensions.hosting.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.http/10.0.3/microsoft.extensions.http.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging/10.0.3/microsoft.extensions.logging.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging.abstractions/10.0.3/microsoft.extensions.logging.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging.configuration/10.0.3/microsoft.extensions.logging.configuration.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging.console/10.0.3/microsoft.extensions.logging.console.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging.debug/10.0.3/microsoft.extensions.logging.debug.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging.eventlog/10.0.3/microsoft.extensions.logging.eventlog.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.logging.eventsource/10.0.3/microsoft.extensions.logging.eventsource.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.objectpool/7.0.0/microsoft.extensions.objectpool.7.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.options/10.0.3/microsoft.extensions.options.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.options.configurationextensions/10.0.3/microsoft.extensions.options.configurationextensions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.extensions.primitives/10.0.3/microsoft.extensions.primitives.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.net.test.sdk/18.0.1/microsoft.net.test.sdk.18.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.netcore.platforms/1.1.0/microsoft.netcore.platforms.1.1.0.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.openapi/1.6.22/microsoft.openapi.1.6.22.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.testplatform.objectmodel/18.0.1/microsoft.testplatform.objectmodel.18.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.testplatform.testhost/18.0.1/microsoft.testplatform.testhost.18.0.1.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.win32.systemevents/9.0.2/microsoft.win32.systemevents.9.0.2.nupkg.sha512",
"/home/wjones/.nuget/packages/moq/4.18.4/moq.4.18.4.nupkg.sha512",
"/home/wjones/.nuget/packages/morestachio/5.0.1.631/morestachio.5.0.1.631.nupkg.sha512",
"/home/wjones/.nuget/packages/nebml/1.1.0.5/nebml.1.1.0.5.nupkg.sha512",
"/home/wjones/.nuget/packages/netstandard.library/1.6.1/netstandard.library.1.6.1.nupkg.sha512",
"/home/wjones/.nuget/packages/newtonsoft.json/13.0.4/newtonsoft.json.13.0.4.nupkg.sha512",
"/home/wjones/.nuget/packages/playlistsnet/1.4.1/playlistsnet.1.4.1.nupkg.sha512",
"/home/wjones/.nuget/packages/polly/8.6.5/polly.8.6.5.nupkg.sha512",
"/home/wjones/.nuget/packages/polly.core/8.6.5/polly.core.8.6.5.nupkg.sha512",
"/home/wjones/.nuget/packages/prometheus-net/8.2.1/prometheus-net.8.2.1.nupkg.sha512",
"/home/wjones/.nuget/packages/prometheus-net.aspnetcore/8.2.1/prometheus-net.aspnetcore.8.2.1.nupkg.sha512",
"/home/wjones/.nuget/packages/prometheus-net.dotnetruntime/4.4.1/prometheus-net.dotnetruntime.4.4.1.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog/4.3.0/serilog.4.3.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.aspnetcore/10.0.0/serilog.aspnetcore.10.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.enrichers.thread/4.0.0/serilog.enrichers.thread.4.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.expressions/5.0.0/serilog.expressions.5.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.extensions.hosting/10.0.0/serilog.extensions.hosting.10.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.extensions.logging/10.0.0/serilog.extensions.logging.10.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.formatting.compact/3.0.0/serilog.formatting.compact.3.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.settings.configuration/10.0.0/serilog.settings.configuration.10.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.sinks.async/2.1.0/serilog.sinks.async.2.1.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.sinks.console/6.1.1/serilog.sinks.console.6.1.1.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.sinks.debug/3.0.0/serilog.sinks.debug.3.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.sinks.file/7.0.0/serilog.sinks.file.7.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/serilog.sinks.graylog/3.1.1/serilog.sinks.graylog.3.1.1.nupkg.sha512",
"/home/wjones/.nuget/packages/seriloganalyzer/0.15.0/seriloganalyzer.0.15.0.nupkg.sha512",
"/home/wjones/.nuget/packages/shimskiasharp/3.4.1/shimskiasharp.3.4.1.nupkg.sha512",
"/home/wjones/.nuget/packages/skiasharp/3.116.1/skiasharp.3.116.1.nupkg.sha512",
"/home/wjones/.nuget/packages/skiasharp.harfbuzz/3.116.1/skiasharp.harfbuzz.3.116.1.nupkg.sha512",
"/home/wjones/.nuget/packages/skiasharp.nativeassets.linux/3.116.1/skiasharp.nativeassets.linux.3.116.1.nupkg.sha512",
"/home/wjones/.nuget/packages/skiasharp.nativeassets.macos/3.116.1/skiasharp.nativeassets.macos.3.116.1.nupkg.sha512",
"/home/wjones/.nuget/packages/skiasharp.nativeassets.win32/3.116.1/skiasharp.nativeassets.win32.3.116.1.nupkg.sha512",
"/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31/smartanalyzers.multithreadinganalyzer.1.1.31.nupkg.sha512",
"/home/wjones/.nuget/packages/sqlitepclraw.bundle_e_sqlite3/2.1.11/sqlitepclraw.bundle_e_sqlite3.2.1.11.nupkg.sha512",
"/home/wjones/.nuget/packages/sqlitepclraw.core/2.1.11/sqlitepclraw.core.2.1.11.nupkg.sha512",
"/home/wjones/.nuget/packages/sqlitepclraw.lib.e_sqlite3/2.1.11/sqlitepclraw.lib.e_sqlite3.2.1.11.nupkg.sha512",
"/home/wjones/.nuget/packages/sqlitepclraw.provider.e_sqlite3/2.1.11/sqlitepclraw.provider.e_sqlite3.2.1.11.nupkg.sha512",
"/home/wjones/.nuget/packages/stylecop.analyzers/1.2.0-beta.556/stylecop.analyzers.1.2.0-beta.556.nupkg.sha512",
"/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556/stylecop.analyzers.unstable.1.2.0.556.nupkg.sha512",
"/home/wjones/.nuget/packages/svg.custom/3.4.1/svg.custom.3.4.1.nupkg.sha512",
"/home/wjones/.nuget/packages/svg.model/3.4.1/svg.model.3.4.1.nupkg.sha512",
"/home/wjones/.nuget/packages/svg.skia/3.4.1/svg.skia.3.4.1.nupkg.sha512",
"/home/wjones/.nuget/packages/swashbuckle.aspnetcore/7.3.2/swashbuckle.aspnetcore.7.3.2.nupkg.sha512",
"/home/wjones/.nuget/packages/swashbuckle.aspnetcore.redoc/6.9.0/swashbuckle.aspnetcore.redoc.6.9.0.nupkg.sha512",
"/home/wjones/.nuget/packages/swashbuckle.aspnetcore.swagger/7.3.2/swashbuckle.aspnetcore.swagger.7.3.2.nupkg.sha512",
"/home/wjones/.nuget/packages/swashbuckle.aspnetcore.swaggergen/7.3.2/swashbuckle.aspnetcore.swaggergen.7.3.2.nupkg.sha512",
"/home/wjones/.nuget/packages/swashbuckle.aspnetcore.swaggerui/7.3.2/swashbuckle.aspnetcore.swaggerui.7.3.2.nupkg.sha512",
"/home/wjones/.nuget/packages/system.diagnostics.eventlog/10.0.3/system.diagnostics.eventlog.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/system.drawing.common/9.0.2/system.drawing.common.9.0.2.nupkg.sha512",
"/home/wjones/.nuget/packages/taglibsharp/2.3.0/taglibsharp.2.3.0.nupkg.sha512",
"/home/wjones/.nuget/packages/tmdblib/2.3.0/tmdblib.2.3.0.nupkg.sha512",
"/home/wjones/.nuget/packages/ude.netstandard/1.2.0/ude.netstandard.1.2.0.nupkg.sha512",
"/home/wjones/.nuget/packages/utf.unknown/2.6.0/utf.unknown.2.6.0.nupkg.sha512",
"/home/wjones/.nuget/packages/validation/2.6.68/validation.2.6.68.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit/2.9.3/xunit.2.9.3.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.abstractions/2.0.3/xunit.abstractions.2.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.analyzers/1.18.0/xunit.analyzers.1.18.0.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.assert/2.9.3/xunit.assert.2.9.3.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.core/2.9.3/xunit.core.2.9.3.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.extensibility.core/2.9.3/xunit.extensibility.core.2.9.3.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.extensibility.execution/2.9.3/xunit.extensibility.execution.2.9.3.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.priority/1.1.6/xunit.priority.1.1.6.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.runner.visualstudio/2.8.2/xunit.runner.visualstudio.2.8.2.nupkg.sha512",
"/home/wjones/.nuget/packages/xunit.skippablefact/1.5.61/xunit.skippablefact.1.5.61.nupkg.sha512",
"/home/wjones/.nuget/packages/z440.atl.core/7.11.0/z440.atl.core.7.11.0.nupkg.sha512",
"/home/wjones/.nuget/packages/zlib.net-mutliplatform/1.0.8/zlib.net-mutliplatform.1.0.8.nupkg.sha512"
],
"logs": []
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
17714532028800000
@@ -0,0 +1 @@
17715044193700000