using System; using System.Collections.Generic; using Jellyfin.Plugin.WatchedTogether; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Cryptography; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace Jellyfin.Plugin.WatchedTogether.Tests; /// /// Guards the plugin's service graph against container-level cycles. /// /// /// Jellyfin's real UserManager constructor-injects IEnumerable<IAuthenticationProvider>. /// That means any plugin service reachable eagerly from our authentication provider must not itself /// require , or the host dies at startup with "a circular dependency was /// detected". A cycle like that is invisible to unit tests that construct services by hand, so these /// tests build the graph the way the host does. /// public class ServiceRegistrationTests { /// /// Stands in for Jellyfin's UserManager, whose constructor takes every registered authentication /// provider. Only the constructor shape matters here - it is what closes the cycle. /// private sealed class UserManagerWithAuthProviders { public UserManagerWithAuthProviders(IEnumerable authenticationProviders) { AuthenticationProviders = authenticationProviders; } public IEnumerable AuthenticationProviders { get; } } private static ServiceProvider BuildHostLikeProvider() { var services = new ServiceCollection(); services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); // Host services the plugin consumes, other than IUserManager. services.AddSingleton(Mock.Of()); services.AddSingleton(Mock.Of()); services.AddSingleton(Mock.Of()); // IUserManager resolves through the fake UserManager so that building it forces every // IAuthenticationProvider to be built first, exactly as the real host does. services.AddSingleton(); services.AddSingleton(provider => { provider.GetRequiredService(); return Mock.Of(); }); new ServiceRegistrator().RegisterServices(services, Mock.Of()); return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }); } [Fact] public void PluginServices_ResolveWithoutCircularDependency() { using var provider = BuildHostLikeProvider(); // Resolving IUserManager is what the host does during startup, and is the exact path that // previously threw InvalidOperationException for a circular dependency. var userManager = provider.GetRequiredService(); Assert.NotNull(userManager); } [Fact] public void AuthenticationProvider_IsConstructedWithoutResolvingUserManager() { using var provider = BuildHostLikeProvider(); var authProviders = provider.GetRequiredService>(); Assert.Contains(authProviders, p => p is Auth.SharedAccountAuthenticationProvider); } [Fact] public void GroupServices_AreStillResolvableOnceTheHostIsUp() { using var provider = BuildHostLikeProvider(); // The Lazy indirection must not change what the services resolve to at authentication // time, and must hand back the same singletons the rest of the plugin uses. var lazyGroupService = provider.GetRequiredService>(); var lazyDynamicGroupService = provider.GetRequiredService>(); Assert.Same(provider.GetRequiredService(), lazyGroupService.Value); Assert.Same(provider.GetRequiredService(), lazyDynamicGroupService.Value); } }