diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs new file mode 100644 index 0000000000..a2f5c25016 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth; +using Jellyfin.Api.Constants; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth +{ + public class CustomAuthenticationHandlerTests + { + private readonly IFixture _fixture; + + private readonly Mock _jellyfinAuthServiceMock; + private readonly Mock> _optionsMonitorMock; + private readonly Mock _clockMock; + private readonly Mock _serviceProviderMock; + private readonly Mock _authenticationServiceMock; + private readonly UrlEncoder _urlEncoder; + private readonly HttpContext _context; + + private readonly CustomAuthenticationHandler _sut; + private readonly AuthenticationScheme _scheme; + + public CustomAuthenticationHandlerTests() + { + var fixtureCustomizations = new AutoMoqCustomization + { + ConfigureMembers = true + }; + + _fixture = new Fixture().Customize(fixtureCustomizations); + AllowFixtureCircularDependencies(); + + _jellyfinAuthServiceMock = _fixture.Freeze>(); + _optionsMonitorMock = _fixture.Freeze>>(); + _clockMock = _fixture.Freeze>(); + _serviceProviderMock = _fixture.Freeze>(); + _authenticationServiceMock = _fixture.Freeze>(); + _fixture.Register(() => new NullLoggerFactory()); + + _urlEncoder = UrlEncoder.Default; + + _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService))) + .Returns(_authenticationServiceMock.Object); + + _optionsMonitorMock.Setup(o => o.Get(It.IsAny())) + .Returns(new AuthenticationSchemeOptions + { + ForwardAuthenticate = null + }); + + _context = new DefaultHttpContext + { + RequestServices = _serviceProviderMock.Object + }; + + _scheme = new AuthenticationScheme( + _fixture.Create(), + null, + typeof(CustomAuthenticationHandler)); + + _sut = _fixture.Create(); + _sut.InitializeAsync(_scheme, _context).Wait(); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldFailWithNullUser() + { + _jellyfinAuthServiceMock.Setup( + a => a.Authenticate( + It.IsAny(), + It.IsAny())) + .Returns((User)null); + + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.False(authenticateResult.Succeeded); + Assert.Equal("Invalid user", authenticateResult.Failure.Message); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldFailOnSecurityException() + { + var errorMessage = _fixture.Create(); + + _jellyfinAuthServiceMock.Setup( + a => a.Authenticate( + It.IsAny(), + It.IsAny())) + .Throws(new SecurityException(errorMessage)); + + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.False(authenticateResult.Succeeded); + Assert.Equal(errorMessage, authenticateResult.Failure.Message); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldSucceedWithUser() + { + SetupUser(); + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.True(authenticateResult.Succeeded); + Assert.Null(authenticateResult.Failure); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldAssignNameClaim() + { + var user = SetupUser(); + var authenticateResult = await _sut.AuthenticateAsync(); + + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Name)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin) + { + var user = SetupUser(isAdmin); + var authenticateResult = await _sut.AuthenticateAsync(); + + var expectedRole = user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User; + Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole)); + } + + [Fact] + public async Task HandleAuthenticateAsyncShouldAssignTicketCorrectScheme() + { + SetupUser(); + var authenticatedResult = await _sut.AuthenticateAsync(); + + Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); + } + + private User SetupUser(bool isAdmin = false) + { + var user = _fixture.Create(); + user.Policy.IsAdministrator = isAdmin; + + _jellyfinAuthServiceMock.Setup( + a => a.Authenticate( + It.IsAny(), + It.IsAny())) + .Returns(user); + + return user; + } + + private void AllowFixtureCircularDependencies() + { + // A circular dependency exists in the User entity around parent folders, + // this allows Autofixture to generate a User regardless, rather than throw + // an error. + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs new file mode 100644 index 0000000000..84cdbe360d --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Authorization; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy +{ + public class FirstTimeSetupOrElevatedHandlerTests + { + private readonly Mock _configurationManagerMock; + private readonly List _requirements; + private readonly FirstTimeSetupOrElevatedHandler _sut; + + public FirstTimeSetupOrElevatedHandlerTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + _configurationManagerMock = fixture.Freeze>(); + _requirements = new List {new FirstTimeSetupOrElevatedRequirement()}; + + _sut = fixture.Create(); + } + + [Theory] + [InlineData(UserRoles.Administrator)] + [InlineData(UserRoles.Guest)] + [InlineData(UserRoles.User)] + public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) + { + SetupConfigurationManager(false); + var user = SetupUser(userRole); + var context = new AuthorizationHandlerContext(_requirements, user, null); + + await _sut.HandleAsync(context); + Assert.True(context.HasSucceeded); + } + + [Theory] + [InlineData(UserRoles.Administrator, true)] + [InlineData(UserRoles.Guest, false)] + [InlineData(UserRoles.User, false)] + public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) + { + SetupConfigurationManager(true); + var user = SetupUser(userRole); + var context = new AuthorizationHandlerContext(_requirements, user, null); + + await _sut.HandleAsync(context); + Assert.Equal(shouldSucceed, context.HasSucceeded); + } + + private static ClaimsPrincipal SetupUser(string role) + { + var claims = new[] {new Claim(ClaimTypes.Role, role)}; + var identity = new ClaimsIdentity(claims); + return new ClaimsPrincipal(identity); + } + + private void SetupConfigurationManager(bool startupWizardCompleted) + { + var commonConfiguration = new BaseApplicationConfiguration + { + IsStartupWizardCompleted = startupWizardCompleted + }; + + _configurationManagerMock.Setup(c => c.CommonConfiguration) + .Returns(commonConfiguration); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs new file mode 100644 index 0000000000..e2beea1ad8 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Jellyfin.Api.Auth.RequiresElevationPolicy; +using Jellyfin.Api.Constants; +using Microsoft.AspNetCore.Authorization; +using Xunit; + +namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy +{ + public class RequiresElevationHandlerTests + { + private readonly RequiresElevationHandler _sut; + + public RequiresElevationHandlerTests() + { + _sut = new RequiresElevationHandler(); + } + + [Theory] + [InlineData(UserRoles.Administrator, true)] + [InlineData(UserRoles.User, false)] + [InlineData(UserRoles.Guest, false)] + public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) + { + var requirements = new List {new RequiresElevationRequirement()}; + + var claims = new[] {new Claim(ClaimTypes.Role, role)}; + var identity = new ClaimsIdentity(claims); + var user = new ClaimsPrincipal(identity); + + var context = new AuthorizationHandlerContext(requirements, user, null); + + await _sut.HandleAsync(context); + Assert.Equal(shouldSucceed, context.HasSucceeded); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index e0deeeabb8..1f83489bdf 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -6,6 +6,10 @@ + + + + @@ -15,6 +19,7 @@ +