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);
}
}