Add focused unit tests
This commit is contained in:
@@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D1AD3B53-612
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Multilang", "src\Jellyfin.Plugin.Multilang\Jellyfin.Plugin.Multilang.csproj", "{138DB6FA-E217-41B3-A9D1-F24FB724DD44}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Multilang", "src\Jellyfin.Plugin.Multilang\Jellyfin.Plugin.Multilang.csproj", "{138DB6FA-E217-41B3-A9D1-F24FB724DD44}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CFC94729-5950-416B-AAA2-640C80D578C7}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Multilang.Tests", "tests\Jellyfin.Plugin.Multilang.Tests\Jellyfin.Plugin.Multilang.Tests.csproj", "{C58F148E-F158-4D78-B3C1-2B3317B24A12}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -20,8 +24,13 @@ Global
|
|||||||
{138DB6FA-E217-41B3-A9D1-F24FB724DD44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{138DB6FA-E217-41B3-A9D1-F24FB724DD44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{138DB6FA-E217-41B3-A9D1-F24FB724DD44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{138DB6FA-E217-41B3-A9D1-F24FB724DD44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{138DB6FA-E217-41B3-A9D1-F24FB724DD44}.Release|Any CPU.Build.0 = Release|Any CPU
|
{138DB6FA-E217-41B3-A9D1-F24FB724DD44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C58F148E-F158-4D78-B3C1-2B3317B24A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C58F148E-F158-4D78-B3C1-2B3317B24A12}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C58F148E-F158-4D78-B3C1-2B3317B24A12}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C58F148E-F158-4D78-B3C1-2B3317B24A12}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{138DB6FA-E217-41B3-A9D1-F24FB724DD44} = {D1AD3B53-6123-4F98-87D5-517B5BF6ADAF}
|
{138DB6FA-E217-41B3-A9D1-F24FB724DD44} = {D1AD3B53-6123-4F98-87D5-517B5BF6ADAF}
|
||||||
|
{C58F148E-F158-4D78-B3C1-2B3317B24A12} = {CFC94729-5950-416B-AAA2-640C80D578C7}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Jellyfin.Plugin.Multilang.Data;
|
||||||
|
using Jellyfin.Plugin.Multilang.Rules;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Multilang.Tests;
|
||||||
|
|
||||||
|
public sealed class CategoryRuleEvaluatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsWithAndListRequiresExactSet()
|
||||||
|
{
|
||||||
|
var category = Rule("production_countries", "is", useOr: false, "SE", "FI");
|
||||||
|
|
||||||
|
Assert.True(CategoryRuleEvaluator.Matches(category, Facts(productionCountries: ["FI", "SE"])));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(category, Facts(productionCountries: ["FI"])));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(category, Facts(productionCountries: ["FI", "SE", "NO"])));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsWithOrListRequiresSingleMatchingValue()
|
||||||
|
{
|
||||||
|
var category = Rule("original_language", "is", useOr: true, "sv", "fi");
|
||||||
|
|
||||||
|
Assert.True(CategoryRuleEvaluator.Matches(category, Facts(originalLanguage: "sv")));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(category, Facts(originalLanguage: "en")));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(category, Facts(originalLanguage: "", spokenLanguages: ["sv", "fi"])));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsWithAndListRequiresEveryExpectedValue()
|
||||||
|
{
|
||||||
|
var category = Rule("spoken_languages", "contains", useOr: false, "sv", "fi");
|
||||||
|
|
||||||
|
Assert.True(CategoryRuleEvaluator.Matches(category, Facts(spokenLanguages: ["en", "sv", "fi"])));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(category, Facts(spokenLanguages: ["en", "sv"])));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsWithOrListRequiresAnyExpectedValue()
|
||||||
|
{
|
||||||
|
var category = Rule("origin_countries", "contains", useOr: true, "SE", "FI");
|
||||||
|
|
||||||
|
Assert.True(CategoryRuleEvaluator.Matches(category, Facts(originCountries: ["NO", "FI"])));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(category, Facts(originCountries: ["NO", "DK"])));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleConditionsRespectCategoryMatchMode()
|
||||||
|
{
|
||||||
|
var all = new UserCategoryRule
|
||||||
|
{
|
||||||
|
Scopes = ["M"],
|
||||||
|
MatchAllConditions = true,
|
||||||
|
Requirements =
|
||||||
|
[
|
||||||
|
Requirement("original_language", "is", false, "sv"),
|
||||||
|
Requirement("production_countries", "contains", true, "SE")
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var any = CopyWithMatchAll(all, false);
|
||||||
|
var swedishLanguageOnly = Facts(originalLanguage: "sv", productionCountries: ["FI"]);
|
||||||
|
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(all, swedishLanguageOnly));
|
||||||
|
Assert.True(CategoryRuleEvaluator.Matches(any, swedishLanguageOnly));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ScopeMustMatchItemKind()
|
||||||
|
{
|
||||||
|
var movieOnly = Rule("original_language", "is", useOr: false, "sv");
|
||||||
|
movieOnly.Scopes = ["M"];
|
||||||
|
|
||||||
|
Assert.True(CategoryRuleEvaluator.Matches(movieOnly, Facts(kind: "movie", originalLanguage: "sv")));
|
||||||
|
Assert.False(CategoryRuleEvaluator.Matches(movieOnly, Facts(kind: "tv", originalLanguage: "sv")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserCategoryRule Rule(string field, string relation, bool useOr, params string[] values)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Scopes = ["M", "S", "C"],
|
||||||
|
Requirements = [Requirement(field, relation, useOr, values)]
|
||||||
|
};
|
||||||
|
|
||||||
|
private static UserCategoryRequirement Requirement(string field, string relation, bool useOr, params string[] values)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Fields = [field],
|
||||||
|
Relation = relation,
|
||||||
|
UseOr = useOr,
|
||||||
|
Values = values
|
||||||
|
};
|
||||||
|
|
||||||
|
private static UserCategoryRule CopyWithMatchAll(UserCategoryRule rule, bool matchAll)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Id = rule.Id,
|
||||||
|
Label = rule.Label,
|
||||||
|
CriteriaText = rule.CriteriaText,
|
||||||
|
Requirements = rule.Requirements,
|
||||||
|
MatchAllConditions = matchAll,
|
||||||
|
Scopes = rule.Scopes,
|
||||||
|
FieldActions = rule.FieldActions,
|
||||||
|
FieldActionLists = rule.FieldActionLists
|
||||||
|
};
|
||||||
|
|
||||||
|
private static FactsData Facts(
|
||||||
|
string kind = "movie",
|
||||||
|
string originalLanguage = "",
|
||||||
|
string[]? originCountries = null,
|
||||||
|
string[]? productionCountries = null,
|
||||||
|
string[]? spokenLanguages = null,
|
||||||
|
string audioLanguage = "")
|
||||||
|
=> new(
|
||||||
|
"item",
|
||||||
|
"tmdb",
|
||||||
|
kind,
|
||||||
|
"Original title",
|
||||||
|
originalLanguage,
|
||||||
|
originalLanguage,
|
||||||
|
JsonSerializer.Serialize(originCountries ?? []),
|
||||||
|
JsonSerializer.Serialize(productionCountries ?? []),
|
||||||
|
JsonSerializer.Serialize(spokenLanguages ?? []),
|
||||||
|
audioLanguage,
|
||||||
|
"[]",
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using Jellyfin.Plugin.Multilang.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Multilang.Tests;
|
||||||
|
|
||||||
|
public sealed class ItemsProxyRequestBuilderTests
|
||||||
|
{
|
||||||
|
private const string UserId = "11111111111111111111111111111111";
|
||||||
|
private const string OtherUserId = "22222222222222222222222222222222";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildNormalizesRootItemsPathToCurrentUser()
|
||||||
|
{
|
||||||
|
var request = HttpRequest();
|
||||||
|
|
||||||
|
var result = ItemsProxyRequestBuilder.Build(
|
||||||
|
request,
|
||||||
|
"/Items?Recursive=true&api_key=old",
|
||||||
|
"token",
|
||||||
|
UserId,
|
||||||
|
multilangEnabled: true);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal($"/Users/{UserId}/Items", result.Upstream.AbsolutePath);
|
||||||
|
Assert.Contains("api_key=token", result.Upstream.Query);
|
||||||
|
Assert.DoesNotContain("api_key=old", result.Upstream.Query);
|
||||||
|
Assert.DoesNotContain("api_key", result.NormalizedUrlForCache, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("userId=" + UserId, result.NormalizedUrlForCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildReplacesForeignUserIdInUpstreamPath()
|
||||||
|
{
|
||||||
|
var request = HttpRequest();
|
||||||
|
|
||||||
|
var result = ItemsProxyRequestBuilder.Build(
|
||||||
|
request,
|
||||||
|
$"/Users/{OtherUserId}/Items?Limit=5",
|
||||||
|
"token",
|
||||||
|
UserId,
|
||||||
|
multilangEnabled: true);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal($"/Users/{UserId}/Items", result.Upstream.AbsolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildKeepsLocalListControlsOutOfUpstreamWhenEnabled()
|
||||||
|
{
|
||||||
|
var request = HttpRequest("?mlLocale=fi-FI");
|
||||||
|
|
||||||
|
var result = ItemsProxyRequestBuilder.Build(
|
||||||
|
request,
|
||||||
|
"/Items?SortBy=SortName&SortOrder=Descending&StartIndex=20&Limit=10&NameStartsWith=L",
|
||||||
|
"token",
|
||||||
|
UserId,
|
||||||
|
multilangEnabled: true);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("SortName", result.Controls.SortBy);
|
||||||
|
Assert.Equal("Descending", result.Controls.SortOrder);
|
||||||
|
Assert.Equal(20, result.Controls.StartIndex);
|
||||||
|
Assert.Equal(10, result.Controls.Limit);
|
||||||
|
Assert.Equal("L", result.Controls.NameStartsWith);
|
||||||
|
Assert.Equal("fi-FI", result.Controls.ClientLocale);
|
||||||
|
Assert.DoesNotContain("SortBy=", result.Upstream.Query);
|
||||||
|
Assert.DoesNotContain("Limit=", result.Upstream.Query);
|
||||||
|
Assert.Contains("|SortName|Descending|L|20|10|fi-FI|", result.CacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildLeavesSortAndPagingUpstreamWhenMultilangDisabled()
|
||||||
|
{
|
||||||
|
var request = HttpRequest();
|
||||||
|
|
||||||
|
var result = ItemsProxyRequestBuilder.Build(
|
||||||
|
request,
|
||||||
|
"/Items?SortBy=SortName&Limit=10",
|
||||||
|
"token",
|
||||||
|
UserId,
|
||||||
|
multilangEnabled: false);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(string.Empty, result.Controls.SortBy);
|
||||||
|
Assert.Equal(0, result.Controls.Limit);
|
||||||
|
Assert.Contains("SortBy=SortName", result.Upstream.Query);
|
||||||
|
Assert.Contains("Limit=10", result.Upstream.Query);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildRejectsAbsoluteUrlForAnotherHost()
|
||||||
|
{
|
||||||
|
var request = HttpRequest();
|
||||||
|
|
||||||
|
var result = ItemsProxyRequestBuilder.Build(
|
||||||
|
request,
|
||||||
|
"http://other-host:8096/Items",
|
||||||
|
"token",
|
||||||
|
UserId,
|
||||||
|
multilangEnabled: true);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseLocalGenreIdsOnlyReturnsTmdbIds()
|
||||||
|
{
|
||||||
|
Assert.Equal([1, 2], ItemsProxyRequestBuilder.ParseLocalGenreIds("tmdb-1,abc,2,tmdb-2,tmdb-x,1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpRequest HttpRequest(string query = "")
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Request.Scheme = "http";
|
||||||
|
context.Request.Host = new HostString("server.local", 8096);
|
||||||
|
context.Request.QueryString = new QueryString(query);
|
||||||
|
return context.Request;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
using Jellyfin.Plugin.Multilang.Data;
|
||||||
|
using Jellyfin.Plugin.Multilang.Services;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Multilang.Tests;
|
||||||
|
|
||||||
|
public sealed class ItemsProxyTransformerResolutionTests
|
||||||
|
{
|
||||||
|
private const string TitleField = "title";
|
||||||
|
private const string OverviewField = "overview";
|
||||||
|
private const string TaglineField = "tagline";
|
||||||
|
private const string PosterKind = "poster";
|
||||||
|
private const string LogoKind = "logo";
|
||||||
|
private const string OriginalAction = "Original";
|
||||||
|
private const string LanguagePrefix = "Language:";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveFieldUsesFirstLanguageWithData()
|
||||||
|
{
|
||||||
|
var translations = TextTranslations(
|
||||||
|
("sv", OverviewField, ""),
|
||||||
|
("fi", OverviewField, "Finnish overview"),
|
||||||
|
("en", OverviewField, "English overview"));
|
||||||
|
|
||||||
|
var result = ItemsProxyTransformer.ResolveField(
|
||||||
|
OverviewField,
|
||||||
|
[LanguagePrefix + "sv", LanguagePrefix + "fi", LanguagePrefix + "en"],
|
||||||
|
Facts(),
|
||||||
|
translations);
|
||||||
|
|
||||||
|
Assert.True(result.Change);
|
||||||
|
Assert.Equal("Finnish overview", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveFieldUsesOriginalTitleBeforeTranslations()
|
||||||
|
{
|
||||||
|
var result = ItemsProxyTransformer.ResolveField(
|
||||||
|
TitleField,
|
||||||
|
[OriginalAction, LanguagePrefix + "en"],
|
||||||
|
Facts(originalTitle: "Original title"),
|
||||||
|
TextTranslations(("en", TitleField, "English title")));
|
||||||
|
|
||||||
|
Assert.True(result.Change);
|
||||||
|
Assert.Equal("Original title", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveFieldFallsBackToJellyfinForMissingTitle()
|
||||||
|
{
|
||||||
|
var result = ItemsProxyTransformer.ResolveField(
|
||||||
|
TitleField,
|
||||||
|
[LanguagePrefix + "sv"],
|
||||||
|
Facts(),
|
||||||
|
TextTranslations());
|
||||||
|
|
||||||
|
Assert.False(result.Change);
|
||||||
|
Assert.Null(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveFieldClearsMissingOptionalText()
|
||||||
|
{
|
||||||
|
var result = ItemsProxyTransformer.ResolveField(
|
||||||
|
TaglineField,
|
||||||
|
[LanguagePrefix + "sv"],
|
||||||
|
Facts(),
|
||||||
|
TextTranslations());
|
||||||
|
|
||||||
|
Assert.True(result.Change);
|
||||||
|
Assert.Equal(string.Empty, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveAssetUsesFirstLanguageWithData()
|
||||||
|
{
|
||||||
|
var assets = AssetTranslations(
|
||||||
|
(PosterKind, "sv", ""),
|
||||||
|
(PosterKind, "fi", "https://image/fi.jpg"),
|
||||||
|
(PosterKind, "en", "https://image/en.jpg"));
|
||||||
|
|
||||||
|
var result = ItemsProxyTransformer.ResolveAsset(
|
||||||
|
PosterKind,
|
||||||
|
[LanguagePrefix + "sv", LanguagePrefix + "fi", LanguagePrefix + "en"],
|
||||||
|
assets);
|
||||||
|
|
||||||
|
Assert.True(result.Change);
|
||||||
|
Assert.Equal("https://image/fi.jpg", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveAssetFallsBackToJellyfinForMissingPoster()
|
||||||
|
{
|
||||||
|
var result = ItemsProxyTransformer.ResolveAsset(
|
||||||
|
PosterKind,
|
||||||
|
[LanguagePrefix + "sv"],
|
||||||
|
AssetTranslations());
|
||||||
|
|
||||||
|
Assert.False(result.Change);
|
||||||
|
Assert.Null(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveAssetClearsMissingOptionalImage()
|
||||||
|
{
|
||||||
|
var result = ItemsProxyTransformer.ResolveAsset(
|
||||||
|
LogoKind,
|
||||||
|
[LanguagePrefix + "sv"],
|
||||||
|
AssetTranslations());
|
||||||
|
|
||||||
|
Assert.True(result.Change);
|
||||||
|
Assert.Equal(string.Empty, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, Dictionary<string, string>> TextTranslations(params (string Lang, string Field, string Value)[] rows)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (lang, field, value) in rows)
|
||||||
|
{
|
||||||
|
if (!result.TryGetValue(lang, out var byField))
|
||||||
|
{
|
||||||
|
byField = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
result[lang] = byField;
|
||||||
|
}
|
||||||
|
|
||||||
|
byField[field] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, Dictionary<string, string>> AssetTranslations(params (string Kind, string Lang, string Value)[] rows)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (kind, lang, value) in rows)
|
||||||
|
{
|
||||||
|
if (!result.TryGetValue(kind, out var byLang))
|
||||||
|
{
|
||||||
|
byLang = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
result[kind] = byLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
byLang[lang] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FactsData Facts(string originalTitle = "")
|
||||||
|
=> new(
|
||||||
|
"item",
|
||||||
|
"tmdb",
|
||||||
|
"movie",
|
||||||
|
originalTitle,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"[]",
|
||||||
|
"[]",
|
||||||
|
"[]",
|
||||||
|
"",
|
||||||
|
"[]",
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Jellyfin.Plugin.Multilang\Jellyfin.Plugin.Multilang.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user