Test project: UT-001..011 for JR-016 and JR-022
The register defined 46 requirements and zero tests, so every Done in it rested on inspection. This adds the T1 tier: an xUnit project in the solution, covering the two units whose logic is pure enough to test without a Jellyfin host. JR-022 (UT-001..006) covers the cases where removal could reach too far -- another plugin's injection, and an unmarked look-alike script tag -- because index.html is a file JRay shares. UT-001 pins the trailing newline to the tag, without which every install cycle leaves another blank line behind. JR-016 (UT-007..011) covers specificity resolution, including the prioritised series inside an ignored genre that motivated the rule, and a Series rule valued Guid.Empty, which would otherwise swallow every movie in the library. RemoveInjection is reached through InternalsVisibleTo rather than being made public: it is factored out for testability, not part of the surface. The suite BUILDS but does not RUN here. Jellyfin.Controller framework- references Microsoft.AspNetCore.App, so the test host demands it even for pure logic, and this machine has no ASP.NET Core runtime at any version -- RollForward cannot substitute for a framework that is absent entirely. Fix is to install aspnet-runtime, which the CI image needs for the same reason. So every UT here is recorded as Written, not Passing, and no requirement is promoted to Done on their strength. A test whose result nobody has seen is not evidence. TRACES: UT-001, UT-002, UT-003, UT-004, UT-005, UT-006 | JR-022 TRACES: UT-007, UT-008, UT-009, UT-010, UT-011 | JR-016 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!--
|
||||
The plugin project treats warnings as errors and runs StyleCop. Tests do
|
||||
not inherit that: their naming conventions differ deliberately (Method_
|
||||
Condition_Expectation reads as documentation, and trips SA1300-family
|
||||
rules), and a style failure in a test is not a defect in the thing under
|
||||
test.
|
||||
-->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<!--
|
||||
The plugin targets net9.0 to match Jellyfin's ABI, but a machine that can
|
||||
build it need not have the 9.0 *runtime* installed. Roll the test host
|
||||
forward to whatever major is present so the suite runs on a developer box
|
||||
and on CI without pinning either to a runtime that is not the plugin's.
|
||||
-->
|
||||
<RollForward>LatestMajor</RollForward>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jellyfin.Plugin.JRay\Jellyfin.Plugin.JRay.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
using Jellyfin.Plugin.JRay.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// JR-016 — prioritise/ignore rules resolve by specificity: Item beats Series
|
||||
/// beats Genre. A scope+value holds only one action, so the only conflicts
|
||||
/// possible are across scopes, and that is exactly what these cover.
|
||||
///
|
||||
/// TRACES: UT-007, UT-008, UT-009, UT-010, UT-011 | JR-016
|
||||
/// </summary>
|
||||
public class PolicyResolverTests
|
||||
{
|
||||
private static readonly Guid ItemId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
private static readonly Guid SeriesId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
private static MediaPolicyRule Rule(PolicyScope scope, string value, PolicyAction action)
|
||||
=> new() { Scope = scope, Value = value, Action = action };
|
||||
|
||||
// UT-007
|
||||
[Fact]
|
||||
public void Resolve_WithNoMatchingRule_ReturnsNull()
|
||||
{
|
||||
var rules = new List<MediaPolicyRule> { Rule(PolicyScope.Genre, "Anime", PolicyAction.Ignore) };
|
||||
|
||||
Assert.Null(PolicyResolver.Resolve(rules, ItemId, SeriesId, new[] { "Drama" }));
|
||||
}
|
||||
|
||||
// UT-008
|
||||
[Fact]
|
||||
public void Resolve_ItemRuleBeatsSeriesRule()
|
||||
{
|
||||
var rules = new List<MediaPolicyRule>
|
||||
{
|
||||
Rule(PolicyScope.Series, SeriesId.ToString("D"), PolicyAction.Ignore),
|
||||
Rule(PolicyScope.Item, ItemId.ToString("D"), PolicyAction.Prioritise),
|
||||
};
|
||||
|
||||
Assert.Equal(PolicyAction.Prioritise, PolicyResolver.Resolve(rules, ItemId, SeriesId, Array.Empty<string>()));
|
||||
}
|
||||
|
||||
// UT-009
|
||||
[Fact]
|
||||
public void Resolve_PrioritisedSeriesInsideIgnoredGenre_SeriesWins()
|
||||
{
|
||||
// The case that motivated specificity resolution: an admin ignores a
|
||||
// whole genre but wants one series out of it anyway. If genre won, the
|
||||
// more specific instruction would be silently discarded.
|
||||
var rules = new List<MediaPolicyRule>
|
||||
{
|
||||
Rule(PolicyScope.Genre, "Anime", PolicyAction.Ignore),
|
||||
Rule(PolicyScope.Series, SeriesId.ToString("D"), PolicyAction.Prioritise),
|
||||
};
|
||||
|
||||
Assert.Equal(PolicyAction.Prioritise, PolicyResolver.Resolve(rules, ItemId, SeriesId, new[] { "Anime" }));
|
||||
}
|
||||
|
||||
// UT-010
|
||||
[Fact]
|
||||
public void Resolve_GenreMatchIsCaseInsensitive()
|
||||
{
|
||||
var rules = new List<MediaPolicyRule> { Rule(PolicyScope.Genre, "anime", PolicyAction.Ignore) };
|
||||
|
||||
Assert.Equal(PolicyAction.Ignore, PolicyResolver.Resolve(rules, ItemId, SeriesId, new[] { "AnImE" }));
|
||||
}
|
||||
|
||||
// UT-011
|
||||
[Fact]
|
||||
public void Resolve_SeriesRuleDoesNotMatchNonEpisode()
|
||||
{
|
||||
// A movie carries Guid.Empty as its series id. A series rule whose value
|
||||
// happened to be an empty GUID must not swallow every movie in the
|
||||
// library.
|
||||
var rules = new List<MediaPolicyRule>
|
||||
{
|
||||
Rule(PolicyScope.Series, Guid.Empty.ToString("D"), PolicyAction.Ignore),
|
||||
};
|
||||
|
||||
Assert.Null(PolicyResolver.Resolve(rules, ItemId, Guid.Empty, Array.Empty<string>()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Jellyfin.Plugin.JRay.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// JR-022 — an earlier JRay injected its overlay script into index.html on
|
||||
/// disk. Those users must not be left with a stale injection pointing at
|
||||
/// endpoints that have since changed, so removal survives even though JR-021
|
||||
/// deleted the injection that created it.
|
||||
///
|
||||
/// Removal keys on JRay's own marker. The cases that matter are the ones where
|
||||
/// it could reach too far: another plugin's injection, or a script tag that
|
||||
/// looks like JRay's but carries no marker.
|
||||
///
|
||||
/// TRACES: UT-001, UT-002, UT-003, UT-004, UT-005 | JR-022
|
||||
/// </summary>
|
||||
public class WebClientPatchServiceTests
|
||||
{
|
||||
private const string Marker = "<!-- jray-overlay -->";
|
||||
private const string ScriptTag = "<script defer src=\"/Plugins/JRay/ClientScript\"></script>";
|
||||
|
||||
// UT-001
|
||||
[Fact]
|
||||
public void RemoveInjection_WithMarkedTagAndNewline_RestoresOriginalBytes()
|
||||
{
|
||||
var original = "<html><body><div>x</div>\n</body></html>";
|
||||
var patched = "<html><body><div>x</div>\n" + ScriptTag + Marker + "\n</body></html>";
|
||||
|
||||
// The trailing newline goes with the tag. If it did not, every
|
||||
// install/uninstall cycle would leave another blank line behind.
|
||||
Assert.Equal(original, WebClientPatchService.RemoveInjection(patched));
|
||||
}
|
||||
|
||||
// UT-002
|
||||
[Fact]
|
||||
public void RemoveInjection_WithMarkedTagAndNoNewline_RemovesTag()
|
||||
{
|
||||
var patched = "<html><body>" + ScriptTag + Marker + "</body></html>";
|
||||
|
||||
Assert.Equal("<html><body></body></html>", WebClientPatchService.RemoveInjection(patched));
|
||||
}
|
||||
|
||||
// UT-003
|
||||
[Fact]
|
||||
public void RemoveInjection_WithNoMarker_LeavesDocumentUnchanged()
|
||||
{
|
||||
var clean = "<html><body><div>x</div>\n</body></html>";
|
||||
|
||||
Assert.Equal(clean, WebClientPatchService.RemoveInjection(clean));
|
||||
}
|
||||
|
||||
// UT-004
|
||||
[Fact]
|
||||
public void RemoveInjection_IsIdempotent()
|
||||
{
|
||||
var patched = "<html><body>" + ScriptTag + Marker + "\n</body></html>";
|
||||
|
||||
var once = WebClientPatchService.RemoveInjection(patched);
|
||||
var twice = WebClientPatchService.RemoveInjection(once);
|
||||
|
||||
// Startup calls this unconditionally, so it runs on every boot forever
|
||||
// after the patch is gone.
|
||||
Assert.Equal(once, twice);
|
||||
}
|
||||
|
||||
// UT-005
|
||||
[Fact]
|
||||
public void RemoveInjection_LeavesAnotherPluginsInjectionIntact()
|
||||
{
|
||||
var foreign = "<script defer src=\"/Plugins/Other/Script\"></script><!-- other-overlay -->";
|
||||
var patched = "<html><body>" + foreign + ScriptTag + Marker + "\n</body></html>";
|
||||
|
||||
var cleaned = WebClientPatchService.RemoveInjection(patched);
|
||||
|
||||
// The marker is what makes removal unambiguous. Removing anything we did
|
||||
// not add is the failure this guards: it is another plugin's file too.
|
||||
Assert.Contains(foreign, cleaned, System.StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(Marker, cleaned, System.StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// UT-006
|
||||
[Fact]
|
||||
public void RemoveInjection_WithUnmarkedLookalikeTag_LeavesItAlone()
|
||||
{
|
||||
// Same script tag, no marker: JRay did not write this, so JRay does not
|
||||
// remove it.
|
||||
var patched = "<html><body>" + ScriptTag + "\n</body></html>";
|
||||
|
||||
Assert.Equal(patched, WebClientPatchService.RemoveInjection(patched));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user