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([]));
}
}