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; /// /// Covers the rule that a shared account sees only what every member can already see. /// 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]; /// /// Creates a user with either full access or an explicit library list. /// private static User MakeUser( string name, bool allFolders = false, IEnumerable? enabled = null, IEnumerable? 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(); 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(); folder.Object.Id = id; return folder.Object; }).ToList(); var root = new Mock(); root.Setup(r => r.Children).Returns(children); var libraryManager = new Mock(); libraryManager.Setup(l => l.GetUserRootFolder()).Returns(root.Object); return new LibraryAccessService( userManager.Object, libraryManager.Object, NullLogger.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]); Assert.Equal([Shows, Kids], result.OrderBy(g => g).ToList().OrderBy(g => g).ToList()); 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([])); } }