aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs')
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs179
1 files changed, 176 insertions, 3 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index d973076ed3..4e57f32b57 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -6,6 +6,7 @@ using BitFaster.Caching;
using Emby.Server.Implementations.Localization;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -180,7 +181,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(24, ratings.Count);
+ Assert.Equal(34, ratings.Count);
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);
@@ -199,6 +200,47 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
[InlineData("Rated: R", "US", 17, 0)]
[InlineData("Rated R", "US", 17, 0)]
[InlineData(" PG-13 ", "US", 13, 0)]
+ [InlineData("T", "IT", 0, null)]
+ [InlineData("VM6", "IT", 6, null)]
+ [InlineData("VM12", "IT", 12, null)]
+ [InlineData("VM14", "IT", 14, null)]
+ [InlineData("VM18", "IT", 18, null)]
+ [InlineData("IT-VM14", "IT", 14, null)] // TMDB style country prefix
+ [InlineData("IT-VM18", "IT", 18, null)]
+ [InlineData("it-vm18", "IT", 18, null)] // Rating strings are case insensitive
+ [InlineData("VM 18", "IT", 18, null)]
+ [InlineData("Vietato ai minori di 18 anni", "IT", 18, null)]
+ [InlineData("ATP", "AR", 0, null)]
+ [InlineData("SAM 13", "AR", 13, null)]
+ [InlineData("SAM 16", "AR", 16, null)]
+ [InlineData("SAM 18", "AR", 18, null)]
+ [InlineData("SAM13", "AR", 13, null)] // Written without a space
+ [InlineData("AR-SAM 16", "AR", 16, null)] // Country prefix stripped against the configured country
+ [InlineData("AR-SAM 13", "US", 13, null)] // Country prefix resolved via the separator fallback
+ [InlineData("AR-SAM 18", "US", 18, null)]
+ [InlineData("AR-SAM13", "US", 13, null)]
+ [InlineData("SAM 18 C", "AR", 1001, null)] // Condicionada, same as "C"
+ [InlineData("Interdit aux moins de 12 ans", "FR", 12, null)]
+ [InlineData("Interdit aux moins de 18 ans", "FR", 18, null)]
+ [InlineData("X 18+", "AU", 1000, 0)] // Official spelling of the Australian X rating
+ [InlineData("X18+", "AU", 1000, 0)]
+ [InlineData("FSK18", "DE", 18, null)] // Written without a space
+ [InlineData("ab 18", "DE", 18, null)] // Written as the minimum age
+ [InlineData("DE:ab 6", "DE", 6, null)]
+ [InlineData("–12", "FR", 12, null)] // The CNC writes its minimum ages with an en dash
+ [InlineData("–16", "FR", 16, null)]
+ [InlineData("–18", "FR", 18, null)]
+ [InlineData("SU", "ID", 0, null)] // Indonesian broadcast classifications (KPI)
+ [InlineData("P", "ID", 2, null)]
+ [InlineData("A", "ID", 7, null)]
+ [InlineData("R", "ID", 13, null)]
+ [InlineData("D", "ID", 18, null)]
+ [InlineData("D18+", "ID", 18, null)] // Written with the minimum age, as broadcast since 2016
+ [InlineData("R-BO", "ID", 13, null)] // Parental guidance does not change the age group
+ [InlineData("Dewasa", "ID", 18, null)]
+ [InlineData("ID-D", "ID", 18, null)] // TMDB style country prefix
+ [InlineData("ID-D", "US", 18, null)] // Country prefix resolved via the separator fallback
+ [InlineData("P", "US", 1000, null)] // Ambiguous outside Indonesia, preferred as the Portuguese "Pornográfico"
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int? expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration()
@@ -213,12 +255,42 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
}
[Theory]
+ // Rating strings are stored mixed-case in the *.json rating systems and must match regardless of casing
+ [InlineData("btl", "se", 0, null)] // Direct lookup, lowercase of "Btl"
+ [InlineData("BARNTILLÅTEN", "se", 0, null)] // Direct lookup, uppercase incl. diacritics
+ [InlineData("SE-BTL", "se", 0, null)] // Country prefix stripped against the configured country
+ [InlineData("SE-BTL", "us", 0, null)] // Country prefix resolved via the separator fallback
+ [InlineData("Från 7 År", "se", 7, null)] // Diacritic casing (json has "Från 7 år")
+ [InlineData("SE-Från 7 År", "us", 7, null)] // Same, via the separator fallback
+ [InlineData("fsk-16", "de", 16, null)] // Not Sweden specific: lowercase of "FSK-16"
+ public async Task GetRatingScore_IsCaseInsensitive_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)]
+ // The French CNC writes minimum ages as "-12" ("not for under 12s"). Parsing that as -12 would
+ // put the item below every MaxParentalRatingScore and bypass parental control entirely.
+ [InlineData("-10", 10, null)]
+ [InlineData("-12", 12, null)]
+ [InlineData("-16", 16, null)]
+ [InlineData("-18", 18, null)]
public async Task GetRatingLevel_GivenValidAge_Success(string value, int? expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "nl" });
@@ -241,6 +313,107 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Null(localizationManager.GetRatingScore("unrated"));
Assert.Null(localizationManager.GetRatingScore("Not Rated"));
Assert.Null(localizationManager.GetRatingScore("n/a"));
+ Assert.Null(localizationManager.GetRatingScore("N/A"));
+ Assert.Null(localizationManager.GetRatingScore(" n/a "));
+ }
+
+ [Theory]
+ // "NR" and "UR" are rating strings of some systems, so they must stay unrated when listed alongside others
+ [InlineData("NR / R", 17, 0)]
+ [InlineData("unrated / R", 17, 0)]
+ [InlineData("R / NR", 17, 0)]
+ public async Task GetRatingLevel_SkipsUnratedListEntries_Success(string value, int? expectedScore, int? expectedSubScore)
+ {
+ var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "us" });
+ await localizationManager.LoadAll();
+
+ var score = localizationManager.GetRatingScore(value);
+
+ Assert.NotNull(score);
+ Assert.Equal(expectedScore, score.Score);
+ Assert.Equal(expectedSubScore, score.SubScore);
+ }
+
+ [Theory]
+ // Ratings that contain a '/' themselves must not be split into a list of ratings
+ [InlineData("M/3", "pt", 3, null)]
+ [InlineData("M/12", "pt", 12, null)]
+ [InlineData("M/18", "pt", 18, null)]
+ [InlineData("PT-M/12", "pt", 12, null)] // TMDB style country prefix
+ [InlineData("M/12", "us", 12, null)] // Resolved through the all-systems fallback
+ [InlineData("U/A 13+", "in", 13, null)]
+ [InlineData("7/i", "es", 11, null)]
+ [InlineData("7/i/fig", "es", 11, null)]
+ [InlineData("18/fig", "es", 18, null)]
+ public async Task GetRatingScore_RatingContainingSlash_IsNotSplit(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]
+ // Providers list every spelling of a rating in a single field. Splitting such a list by its country
+ // prefix pairs the first entry's country with the last entry's rating, so it has to be split by '/' first.
+ [InlineData("DE:FSK 18 / DE:FSK-18 / DE:FSK18 / DE:18 / DE:ab 18", "de", 18, null)]
+ [InlineData("SE:15 / SE:15+ / SE:Från 15 år", "de", 15, null)]
+ [InlineData("FR:16 / US:12", "de", 16, null)] // The first entry that resolves wins
+ public async Task GetRatingScore_CountryPrefixedList_UsesFirstResolvingEntry(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);
+ }
+
+ [Fact]
+ public async Task GetRatingScore_ResolvedCountryPrefixedList_DoesNotWarn()
+ {
+ var logger = new Mock<ILogger<LocalizationManager>>();
+ var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "de" }, logger.Object);
+ await localizationManager.LoadAll();
+
+ Assert.NotNull(localizationManager.GetRatingScore("DE:FSK 18 / DE:FSK-18 / DE:FSK18 / DE:18 / DE:ab 18"));
+
+ logger.Verify(
+ x => x.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.IsAny<It.IsAnyType>(),
+ It.IsAny<Exception?>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Never);
+ }
+
+ [Fact]
+ public async Task GetRatingScore_ListWithoutKnownRating_WarnsOnce()
+ {
+ var logger = new Mock<ILogger<LocalizationManager>>();
+ var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "de" }, logger.Object);
+ await localizationManager.LoadAll();
+
+ Assert.Null(localizationManager.GetRatingScore("DE:Unbekannt / DE:Unsinn"));
+
+ logger.Verify(
+ x => x.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.IsAny<It.IsAnyType>(),
+ It.IsAny<Exception?>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Once);
}
[Theory]
@@ -446,12 +619,12 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Contains(supported, c => c.Name.Equals("es-419", StringComparison.OrdinalIgnoreCase));
}
- private LocalizationManager Setup(ServerConfiguration config)
+ private LocalizationManager Setup(ServerConfiguration config, ILogger<LocalizationManager>? logger = null)
{
var mockConfiguration = new Mock<IServerConfigurationManager>();
mockConfiguration.SetupGet(x => x.Configuration).Returns(config);
- return new LocalizationManager(mockConfiguration.Object, new NullLogger<LocalizationManager>());
+ return new LocalizationManager(mockConfiguration.Object, logger ?? new NullLogger<LocalizationManager>());
}
}
}