Files
dtourolle a2780915bb
🏗️ Build Plugin / build (push) Successful in 1m10s
🧪 Test Plugin / test (push) Successful in 35s
🚀 Release Plugin / build-and-release (push) Failing after 33s
Install nodejs in the builder image and fix a flaky intersection test
Two CI fixes.

The builder image lacked nodejs, so every job died at the first step with
"exec: node: executable file not found in $PATH" (exit 127).
actions/checkout and actions/cache are JavaScript actions: the runner
execs node inside the job container to run them, so the image needs it
even though the build itself does not. Verified by exec'ing node with no
shell, which is how the runner invokes it, and by pulling the pushed
image back from the registry.

TwoExplicitLists_IntersectToTheCommonLibraries compared a sorted actual
against an unsorted hardcoded expected, so it only passed when the
randomly generated library GUIDs happened to sort that way - it failed
about half of all runs and would have made CI intermittently red. The
intersection is a set, so it now asserts on membership and count rather
than ordering. Confirmed with 10 consecutive clean runs, up from ~50%.
2026-07-30 00:10:34 +02:00

172 lines
5.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Plugin.WatchedTogether.Services;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Plugin.WatchedTogether.Tests;
/// <summary>
/// Covers the rule that a shared account sees only what every member can already see.
/// </summary>
public class LibraryAccessTests
{
private static readonly Guid Movies = Guid.NewGuid();
private static readonly Guid Shows = Guid.NewGuid();
private static readonly Guid Kids = Guid.NewGuid();
private static readonly Guid Adult = Guid.NewGuid();
private static readonly Guid[] AllLibraries = [Movies, Shows, Kids, Adult];
/// <summary>
/// Creates a user with either full access or an explicit library list.
/// </summary>
private static User MakeUser(
string name,
bool allFolders = false,
IEnumerable<Guid>? enabled = null,
IEnumerable<Guid>? blocked = null)
{
var user = new User(name, "Prov", "ResetProv");
user.SetPermission(PermissionKind.EnableAllFolders, allFolders);
user.SetPreference(PreferenceKind.EnabledFolders, (enabled ?? []).ToArray());
user.SetPreference(PreferenceKind.BlockedMediaFolders, (blocked ?? []).ToArray());
return user;
}
private static LibraryAccessService MakeService(params User[] users)
{
var userManager = new Mock<IUserManager>();
foreach (var u in users)
{
userManager.Setup(m => m.GetUserById(u.Id)).Returns(u);
}
// The root folder's children are the server's top-level libraries.
var children = AllLibraries.Select(id =>
{
var folder = new Mock<BaseItem>();
folder.Object.Id = id;
return folder.Object;
}).ToList();
var root = new Mock<Folder>();
root.Setup(r => r.Children).Returns(children);
var libraryManager = new Mock<ILibraryManager>();
libraryManager.Setup(l => l.GetUserRootFolder()).Returns(root.Object);
return new LibraryAccessService(
userManager.Object,
libraryManager.Object,
NullLogger<LibraryAccessService>.Instance);
}
[Fact]
public void TwoExplicitLists_IntersectToTheCommonLibraries()
{
var alice = MakeUser("alice", enabled: [Movies, Shows, Kids]);
var bob = MakeUser("bob", enabled: [Shows, Kids, Adult]);
var service = MakeService(alice, bob);
var result = service.ComputeIntersection([alice.Id, bob.Id]);
// The intersection is a set: assert on membership, not on ordering. The library ids are
// random GUIDs, so any order-sensitive assertion would pass or fail by luck of the draw.
Assert.Equal(2, result.Count);
Assert.Contains(Shows, result);
Assert.Contains(Kids, result);
Assert.DoesNotContain(Movies, result);
Assert.DoesNotContain(Adult, result);
}
[Fact]
public void AMemberWithAllFolders_DoesNotWidenTheGroup()
{
// The restricted member is what bounds the group, not the permissive one.
var alice = MakeUser("alice", allFolders: true);
var bob = MakeUser("bob", enabled: [Kids]);
var service = MakeService(alice, bob);
var result = service.ComputeIntersection([alice.Id, bob.Id]);
Assert.Equal([Kids], result);
}
[Fact]
public void AllMembersWithAllFolders_GetEveryLibrary()
{
var alice = MakeUser("alice", allFolders: true);
var bob = MakeUser("bob", allFolders: true);
var service = MakeService(alice, bob);
var result = service.ComputeIntersection([alice.Id, bob.Id]);
Assert.Equal(AllLibraries.OrderBy(g => g), result.OrderBy(g => g));
}
[Fact]
public void ABlockedFolder_IsRemovedEvenWithAllFolders()
{
// Blocking is an explicit denial and must survive the "sees everything" permission.
var alice = MakeUser("alice", allFolders: true, blocked: [Adult]);
var bob = MakeUser("bob", allFolders: true);
var service = MakeService(alice, bob);
var result = service.ComputeIntersection([alice.Id, bob.Id]);
Assert.DoesNotContain(Adult, result);
Assert.Contains(Movies, result);
}
[Fact]
public void MembersWithNothingInCommon_GetNoLibraries()
{
var alice = MakeUser("alice", enabled: [Movies]);
var bob = MakeUser("bob", enabled: [Adult]);
var service = MakeService(alice, bob);
Assert.Empty(service.ComputeIntersection([alice.Id, bob.Id]));
}
[Fact]
public void AddingAThirdMember_CanOnlyNarrowAccess()
{
var alice = MakeUser("alice", enabled: [Movies, Shows, Kids]);
var bob = MakeUser("bob", enabled: [Shows, Kids]);
var carol = MakeUser("carol", enabled: [Kids]);
var service = MakeService(alice, bob, carol);
var pair = service.ComputeIntersection([alice.Id, bob.Id]);
var trio = service.ComputeIntersection([alice.Id, bob.Id, carol.Id]);
Assert.Equal(2, pair.Count);
Assert.Equal([Kids], trio);
}
[Fact]
public void AnUnresolvableMember_YieldsNoAccess()
{
// Failing closed: a member we cannot read must not be treated as unrestricted.
var alice = MakeUser("alice", allFolders: true);
var service = MakeService(alice);
Assert.Empty(service.ComputeIntersection([alice.Id, Guid.NewGuid()]));
}
[Fact]
public void NoMembers_YieldsNoAccess()
{
var service = MakeService();
Assert.Empty(service.ComputeIntersection([]));
}
}