OpenID Connect support (#3975)

Co-authored-by: DieselTech <30128380+DieselTech@users.noreply.github.com>
Co-authored-by: majora2007 <josephmajora@gmail.com>
This commit is contained in:
Fesaa
2025-08-03 14:04:33 +02:00
committed by GitHub
parent a9e7581e89
commit b5bfd341d7
80 changed files with 7604 additions and 279 deletions
@@ -0,0 +1,22 @@
#nullable enable
using System;
using API.Entities.Enums;
using API.Extensions;
using Xunit;
namespace API.Tests.Extensions;
public class EnumExtensionTests
{
[Theory]
[InlineData("Early Childhood", AgeRating.EarlyChildhood, true)]
[InlineData("M", AgeRating.Mature, true)]
[InlineData("ThisIsNotAnAgeRating", default(AgeRating), false)]
public void TryParse<TEnum>(string? value, TEnum expected, bool success) where TEnum : struct, Enum
{
Assert.Equal(EnumExtensions.TryParse(value, out TEnum got), success);
Assert.Equal(expected, got);
}
}
+298
View File
@@ -0,0 +1,298 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Builders;
using API.Services;
using API.Services.Tasks.Scanner;
using Kavita.Common;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
namespace API.Tests.Services;
public class AccountServiceTests: AbstractDbTest
{
[Theory]
[InlineData("admin", true)]
[InlineData("^^$SomeBadChars", false)]
[InlineData("Lisa2003", true)]
[InlineData("Kraft Lawrance", false)]
public async Task ValidateUsername_Regex(string username, bool valid)
{
await ResetDb();
var (_, accountService, _, _) = await Setup();
Assert.Equal(valid, !(await accountService.ValidateUsername(username)).Any());
}
[Fact]
public async Task ChangeIdentityProvider_Throws_WhenDefaultAdminUser()
{
await ResetDb();
var (_, accountService, _, _) = await Setup();
var defaultAdmin = await UnitOfWork.UserRepository.GetDefaultAdminUser();
await Assert.ThrowsAsync<KavitaException>(() =>
accountService.ChangeIdentityProvider(defaultAdmin.Id, defaultAdmin, IdentityProvider.Kavita));
}
[Fact]
public async Task ChangeIdentityProvider_Succeeds_WhenSyncUserSettingsIsFalse()
{
await ResetDb();
var (user, accountService, _, _) = await Setup();
var result = await accountService.ChangeIdentityProvider(user.Id, user, IdentityProvider.Kavita);
Assert.False(result);
var updated = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(updated);
Assert.Equal(IdentityProvider.Kavita, updated.IdentityProvider);
}
[Fact]
public async Task ChangeIdentityProvider_Throws_WhenUserIsOidcManaged_AndNoChange()
{
await ResetDb();
var (user, accountService, _, settingsService) = await Setup();
user.IdentityProvider = IdentityProvider.OpenIdConnect;
await UnitOfWork.CommitAsync();
var settings = await UnitOfWork.SettingsRepository.GetSettingsDtoAsync();
settings.OidcConfig.SyncUserSettings = true;
await settingsService.UpdateSettings(settings);
await Assert.ThrowsAsync<KavitaException>(() =>
accountService.ChangeIdentityProvider(user.Id, user, IdentityProvider.OpenIdConnect));
}
[Fact]
public async Task ChangeIdentityProvider_Succeeds_WhenSyncUserSettingsTrue_AndChangeIsAllowed()
{
await ResetDb();
var (user, accountService, _, settingsService) = await Setup();
user.IdentityProvider = IdentityProvider.OpenIdConnect;
await UnitOfWork.CommitAsync();
var settings = await UnitOfWork.SettingsRepository.GetSettingsDtoAsync();
settings.OidcConfig.SyncUserSettings = true;
await settingsService.UpdateSettings(settings);
var result = await accountService.ChangeIdentityProvider(user.Id, user, IdentityProvider.Kavita);
Assert.False(result);
var updated = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(updated);
Assert.Equal(IdentityProvider.Kavita, updated.IdentityProvider);
}
[Fact]
public async Task ChangeIdentityProvider_ReturnsTrue_WhenChangedToOidc()
{
await ResetDb();
var (user, accountService, _, settingsService) = await Setup();
user.IdentityProvider = IdentityProvider.Kavita;
await UnitOfWork.CommitAsync();
var settings = await UnitOfWork.SettingsRepository.GetSettingsDtoAsync();
settings.OidcConfig.SyncUserSettings = true;
await settingsService.UpdateSettings(settings);
var result = await accountService.ChangeIdentityProvider(user.Id, user, IdentityProvider.OpenIdConnect);
Assert.True(result);
var updated = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(updated);
Assert.Equal(IdentityProvider.OpenIdConnect, updated.IdentityProvider);
}
[Fact]
public async Task UpdateLibrariesForUser_GrantsAccessToAllLibraries_WhenAdmin()
{
await ResetDb();
var (user, accountService, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
await accountService.UpdateLibrariesForUser(user, new List<int>(), hasAdminRole: true);
await UnitOfWork.CommitAsync();
var userLibs = await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id);
Assert.Equal(2, userLibs.Count());
}
[Fact]
public async Task UpdateLibrariesForUser_GrantsAccessToSelectedLibraries_WhenNotAdmin()
{
await ResetDb();
var (user, accountService, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
await accountService.UpdateLibrariesForUser(user, new List<int> { mangaLib.Id }, hasAdminRole: false);
await UnitOfWork.CommitAsync();
var userLibs = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
Assert.Single(userLibs);
Assert.Equal(mangaLib.Id, userLibs.First().Id);
}
[Fact]
public async Task UpdateLibrariesForUser_RemovesAccessFromUnselectedLibraries_WhenNotAdmin()
{
await ResetDb();
var (user, accountService, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
// Grant access to both libraries
await accountService.UpdateLibrariesForUser(user, new List<int> { mangaLib.Id, lightNovelsLib.Id }, hasAdminRole: false);
await UnitOfWork.CommitAsync();
var userLibs = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
Assert.Equal(2, userLibs.Count);
// Now restrict access to only light novels
await accountService.UpdateLibrariesForUser(user, new List<int> { lightNovelsLib.Id }, hasAdminRole: false);
await UnitOfWork.CommitAsync();
userLibs = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
Assert.Single(userLibs);
Assert.Equal(lightNovelsLib.Id, userLibs.First().Id);
}
[Fact]
public async Task UpdateLibrariesForUser_GrantsNoLibraries_WhenNoneSelected_AndNotAdmin()
{
await ResetDb();
var (user, accountService, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
// Initially grant access to both libraries
await accountService.UpdateLibrariesForUser(user, new List<int> { mangaLib.Id, lightNovelsLib.Id }, hasAdminRole: false);
await UnitOfWork.CommitAsync();
var userLibs = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
Assert.Equal(2, userLibs.Count);
// Now revoke all access by passing empty list
await accountService.UpdateLibrariesForUser(user, new List<int>(), hasAdminRole: false);
await UnitOfWork.CommitAsync();
userLibs = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
Assert.Empty(userLibs);
}
private async Task<(AppUser, IAccountService, UserManager<AppUser>, SettingsService)> Setup()
{
var defaultAdmin = new AppUserBuilder("defaultAdmin", "defaultAdmin@localhost")
.WithRole(PolicyConstants.AdminRole)
.Build();
var user = new AppUserBuilder("amelia", "amelia@localhost").Build();
var roleStore = new RoleStore<
AppRole,
DataContext,
int,
IdentityUserRole<int>,
IdentityRoleClaim<int>
>(Context);
var roleManager = new RoleManager<AppRole>(
roleStore,
[new RoleValidator<AppRole>()],
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
Substitute.For<ILogger<RoleManager<AppRole>>>());
foreach (var role in PolicyConstants.ValidRoles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new AppRole
{
Name = role,
});
}
}
var userStore = new UserStore<
AppUser,
AppRole,
DataContext,
int,
IdentityUserClaim<int>,
AppUserRole,
IdentityUserLogin<int>,
IdentityUserToken<int>,
IdentityRoleClaim<int>
>(Context);
var userManager = new UserManager<AppUser>(userStore,
new OptionsWrapper<IdentityOptions>(new IdentityOptions()),
new PasswordHasher<AppUser>(),
[new UserValidator<AppUser>()],
[new PasswordValidator<AppUser>()],
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
null!,
Substitute.For<ILogger<UserManager<AppUser>>>());
// Create users with the UserManager such that the SecurityStamp is set
await userManager.CreateAsync(user);
await userManager.CreateAsync(defaultAdmin);
var accountService = new AccountService(userManager, Substitute.For<ILogger<AccountService>>(), UnitOfWork, Mapper, Substitute.For<ILocalizationService>());
var settingsService = new SettingsService(UnitOfWork, Substitute.For<IDirectoryService>(), Substitute.For<ILibraryWatcher>(), Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SettingsService>> (), Substitute.For<IOidcService>());
user = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id, AppUserIncludes.SideNavStreams);
return (user, accountService, userManager, settingsService);
}
protected override async Task ResetDb()
{
Context.AppUser.RemoveRange(Context.AppUser);
Context.Library.RemoveRange(Context.Library);
await UnitOfWork.CommitAsync();
}
}
+582
View File
@@ -0,0 +1,582 @@
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using API.Helpers.Builders;
using API.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
namespace API.Tests.Services;
public class OidcServiceTests: AbstractDbTest
{
[Fact]
public async Task UserSync_Username()
{
await ResetDb();
var (oidcService, _, _, userManager) = await Setup();
var user = new AppUserBuilder("holo", "holo@localhost").Build();
var res = await userManager.CreateAsync(user);
Assert.Empty(res.Errors);
Assert.True(res.Succeeded);
var claims = new List<Claim>()
{
new (ClaimTypes.Name, "amelia"),
new (ClaimTypes.GivenName, "Lawrence"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
// name is updated as the current username is not found, amelia is skipped as it is alredy in use
await oidcService.SyncUserSettings(null!, settings, principal, user);
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal("Lawrence", user.UserName);
claims = new List<Claim>()
{
new (ClaimTypes.Name, "amelia"),
new (ClaimTypes.GivenName, "Lawrence"),
new (ClaimTypes.Surname, "Norah"),
};
identity = new ClaimsIdentity(claims);
principal = new ClaimsPrincipal(identity);
// Ensure a name longer down the list isn't picked if the current username is found
await oidcService.SyncUserSettings(null!, settings, principal, user);
dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal("Lawrence", user.UserName);
}
[Fact]
public async Task UserSync_CustomClaim()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
const string claim = "groups";
var claims = new List<Claim>()
{
new (claim, PolicyConstants.LoginRole),
new (claim, PolicyConstants.DownloadRole),
new (ClaimTypes.Role, PolicyConstants.PromoteRole),
new (claim, OidcService.AgeRestrictionPrefix + "M"),
new (claim, OidcService.LibraryAccessPrefix + "Manga"),
new (ClaimTypes.Role, OidcService.LibraryAccessPrefix + "Light Novels"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
RolesClaim = claim,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
// Check correct roles assigned
var userRoles = await UnitOfWork.UserRepository.GetRoles(user.Id);
Assert.Contains(PolicyConstants.LoginRole, userRoles);
Assert.Contains(PolicyConstants.DownloadRole, userRoles);
Assert.DoesNotContain(PolicyConstants.PromoteRole, userRoles);
// Check correct libraries
var libraries = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).Select(l => l.Name).ToList();
Assert.Single(libraries);
Assert.Contains(mangaLib.Name, libraries);
Assert.DoesNotContain(lightNovelsLib.Name, libraries);
// Check correct age restrictions
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.Mature, dbUser.AgeRestriction);
Assert.False(dbUser.AgeRestrictionIncludeUnknowns);
}
[Fact]
public async Task UserSync_CustomPrefix()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
const string prefix = "kavita-";
var claims = new List<Claim>()
{
new (ClaimTypes.Role, prefix + PolicyConstants.LoginRole),
new (ClaimTypes.Role, prefix + PolicyConstants.DownloadRole),
new (ClaimTypes.Role, PolicyConstants.PromoteRole),
new (ClaimTypes.Role, prefix + OidcService.AgeRestrictionPrefix + "M"),
new (ClaimTypes.Role, prefix + OidcService.LibraryAccessPrefix + "Manga"),
new (ClaimTypes.Role, OidcService.LibraryAccessPrefix + "Light Novels"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
RolesPrefix = prefix,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
// Check correct roles assigned
var userRoles = await UnitOfWork.UserRepository.GetRoles(user.Id);
Assert.Contains(PolicyConstants.LoginRole, userRoles);
Assert.Contains(PolicyConstants.DownloadRole, userRoles);
Assert.DoesNotContain(PolicyConstants.PromoteRole, userRoles);
// Check correct libraries
var libraries = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).Select(l => l.Name).ToList();
Assert.Single(libraries);
Assert.Contains(mangaLib.Name, libraries);
Assert.DoesNotContain(lightNovelsLib.Name, libraries);
// Check correct age restrictions
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.Mature, dbUser.AgeRestriction);
Assert.False(dbUser.AgeRestrictionIncludeUnknowns);
}
[Fact]
public async Task SyncRoles()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var claims = new List<Claim>()
{
new (ClaimTypes.Role, PolicyConstants.LoginRole),
new (ClaimTypes.Role, PolicyConstants.DownloadRole),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var userRoles = await UnitOfWork.UserRepository.GetRoles(user.Id);
Assert.Contains(PolicyConstants.LoginRole, userRoles);
Assert.Contains(PolicyConstants.DownloadRole, userRoles);
// Only give one role
claims = [new Claim(ClaimTypes.Role, PolicyConstants.LoginRole)];
identity = new ClaimsIdentity(claims);
principal = new ClaimsPrincipal(identity);
await oidcService.SyncUserSettings(null!, settings, principal, user);
userRoles = await UnitOfWork.UserRepository.GetRoles(user.Id);
Assert.Contains(PolicyConstants.LoginRole, userRoles);
Assert.DoesNotContain(PolicyConstants.DownloadRole, userRoles);
}
[Fact]
public async Task SyncLibraries()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var mangaLib = new LibraryBuilder("Manga", LibraryType.Manga).Build();
var lightNovelsLib = new LibraryBuilder("Light Novels", LibraryType.LightNovel).Build();
UnitOfWork.LibraryRepository.Add(mangaLib);
UnitOfWork.LibraryRepository.Add(lightNovelsLib);
await UnitOfWork.CommitAsync();
var claims = new List<Claim>()
{
new (ClaimTypes.Role, OidcService.LibraryAccessPrefix + "Manga"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var libraries = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).Select(l => l.Name).ToList();
Assert.Single(libraries);
Assert.Contains(mangaLib.Name, libraries);
Assert.DoesNotContain(lightNovelsLib.Name, libraries);
// Only give access to the other library
claims = [new Claim(ClaimTypes.Role, OidcService.LibraryAccessPrefix + "Light Novels")];
identity = new ClaimsIdentity(claims);
principal = new ClaimsPrincipal(identity);
await oidcService.SyncUserSettings(null!, settings, principal, user);
// Check access has swicthed
libraries = (await UnitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).Select(l => l.Name).ToList();
Assert.Single(libraries);
Assert.Contains(lightNovelsLib.Name, libraries);
Assert.DoesNotContain(mangaLib.Name, libraries);
}
[Fact]
public async Task SyncAgeRestrictions_NoRestrictions()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var claims = new List<Claim>()
{
new (ClaimTypes.Role, OidcService.AgeRestrictionPrefix + "Not Applicable"),
new(ClaimTypes.Role, OidcService.AgeRestrictionPrefix + OidcService.IncludeUnknowns),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.NotApplicable, dbUser.AgeRestriction);
Assert.True(dbUser.AgeRestrictionIncludeUnknowns);
}
[Fact]
public async Task SyncAgeRestrictions_IncludeUnknowns()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var claims = new List<Claim>()
{
new (ClaimTypes.Role, OidcService.AgeRestrictionPrefix + "M"),
new(ClaimTypes.Role, OidcService.AgeRestrictionPrefix + OidcService.IncludeUnknowns),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.Mature, dbUser.AgeRestriction);
Assert.True(dbUser.AgeRestrictionIncludeUnknowns);
}
[Fact]
public async Task SyncAgeRestriction_AdminNone()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var claims = new List<Claim>()
{
new (ClaimTypes.Role, PolicyConstants.AdminRole),
new (ClaimTypes.Role, OidcService.AgeRestrictionPrefix + "M"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.NotApplicable, dbUser.AgeRestriction);
Assert.True(dbUser.AgeRestrictionIncludeUnknowns);
}
[Fact]
public async Task SyncAgeRestriction_MultipleAgeRestrictionClaims()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var claims = new List<Claim>()
{
new (ClaimTypes.Role, OidcService.AgeRestrictionPrefix + "Teen"),
new (ClaimTypes.Role, OidcService.AgeRestrictionPrefix + "M"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.Mature, dbUser.AgeRestriction);
}
[Fact]
public async Task SyncAgeRestriction_NoAgeRestrictionClaims()
{
await ResetDb();
var (oidcService, user, _, _) = await Setup();
var identity = new ClaimsIdentity([]);
var principal = new ClaimsPrincipal(identity);
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
await oidcService.SyncUserSettings(null!, settings, principal, user);
var dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.NotApplicable, dbUser.AgeRestriction);
Assert.True(dbUser.AgeRestrictionIncludeUnknowns);
// Also default to no restrictions when only include unknowns is present
identity = new ClaimsIdentity([new Claim(ClaimTypes.Role, OidcService.AgeRestrictionPrefix + OidcService.IncludeUnknowns)]);
principal = new ClaimsPrincipal(identity);
await oidcService.SyncUserSettings(null!, settings, principal, user);
dbUser = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(dbUser);
Assert.Equal(AgeRating.NotApplicable, dbUser.AgeRestriction);
Assert.True(dbUser.AgeRestrictionIncludeUnknowns);
}
[Fact]
public async Task SyncUserSettings_DontChangeDefaultAdmin()
{
await ResetDb();
var (oidcService, _, _, userManager) = await Setup();
// Make user default user
var user = await UnitOfWork.UserRepository.GetDefaultAdminUser();
var settings = new OidcConfigDto
{
SyncUserSettings = true,
};
var claims = new List<Claim>()
{
new (ClaimTypes.Role, PolicyConstants.ChangePasswordRole),
new (ClaimTypes.Role, OidcService.AgeRestrictionPrefix + "Teen"),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
await oidcService.SyncUserSettings(null!, settings, principal, user);
var userFromDb = await UnitOfWork.UserRepository.GetUserByIdAsync(user.Id);
Assert.NotNull(userFromDb);
Assert.NotEqual(AgeRating.Teen, userFromDb.AgeRestriction);
var newUser = new AppUserBuilder("NotAnAdmin", "NotAnAdmin@localhost").Build();
var res = await userManager.CreateAsync(newUser);
Assert.Empty(res.Errors);
Assert.True(res.Succeeded);
await oidcService.SyncUserSettings(null!, settings, principal, newUser);
userFromDb = await UnitOfWork.UserRepository.GetUserByIdAsync(newUser.Id);
Assert.NotNull(userFromDb);
Assert.True(await userManager.IsInRoleAsync(newUser, PolicyConstants.ChangePasswordRole));
Assert.Equal(AgeRating.Teen, userFromDb.AgeRestriction);
}
[Fact]
public async Task FindBestAvailableName_NoDuplicates()
{
await ResetDb();
var (oidcService, _, _, userManager) = await Setup();
const string preferredName = "PreferredName";
const string name = "Name";
const string givenName = "GivenName";
const string surname = "Surname";
const string email = "Email";
var claims = new List<Claim>()
{
new(JwtRegisteredClaimNames.PreferredUsername, preferredName),
new(ClaimTypes.Name, name),
new(ClaimTypes.GivenName, givenName),
new(ClaimTypes.Surname, surname),
new(ClaimTypes.Email, email),
};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var bestName = await oidcService.FindBestAvailableName(principal);
Assert.NotNull(bestName);
Assert.Equal(preferredName, bestName);
// Create user with this name to make the method fallback to the next claim
var user = new AppUserBuilder(bestName, bestName).Build();
var res = await userManager.CreateAsync(user);
// This has actual information as to why it would fail, so we check it to make sure if the test fail here we know why
Assert.Empty(res.Errors);
Assert.True(res.Succeeded);
// Fallback to name
bestName = await oidcService.FindBestAvailableName(principal);
Assert.NotNull(bestName);
Assert.Equal(name, bestName);
user = new AppUserBuilder(bestName, bestName).Build();
res = await userManager.CreateAsync(user);
Assert.Empty(res.Errors);
Assert.True(res.Succeeded);
// Fallback to given name
bestName = await oidcService.FindBestAvailableName(principal);
Assert.NotNull(bestName);
Assert.Equal(givenName, bestName);
user = new AppUserBuilder(bestName, bestName).Build();
res = await userManager.CreateAsync(user);
Assert.Empty(res.Errors);
Assert.True(res.Succeeded);
// Fallback to surname
bestName = await oidcService.FindBestAvailableName(principal);
Assert.NotNull(bestName);
Assert.Equal(surname, bestName);
user = new AppUserBuilder(bestName, bestName).Build();
res = await userManager.CreateAsync(user);
Assert.Empty(res.Errors);
Assert.True(res.Succeeded);
// When none are found, returns null
bestName = await oidcService.FindBestAvailableName(principal);
Assert.Null(bestName);
}
private async Task<(OidcService, AppUser, IAccountService, UserManager<AppUser>)> Setup()
{
var defaultAdmin = new AppUserBuilder("defaultAdmin", "defaultAdmin@localhost")
.WithRole(PolicyConstants.AdminRole)
.Build();
var user = new AppUserBuilder("amelia", "amelia@localhost").Build();
var roleStore = new RoleStore<
AppRole,
DataContext,
int,
IdentityUserRole<int>,
IdentityRoleClaim<int>
>(Context);
var roleManager = new RoleManager<AppRole>(
roleStore,
[new RoleValidator<AppRole>()],
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
Substitute.For<ILogger<RoleManager<AppRole>>>());
foreach (var role in PolicyConstants.ValidRoles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new AppRole
{
Name = role,
});
}
}
var userStore = new UserStore<
AppUser,
AppRole,
DataContext,
int,
IdentityUserClaim<int>,
AppUserRole,
IdentityUserLogin<int>,
IdentityUserToken<int>,
IdentityRoleClaim<int>
>(Context);
var userManager = new UserManager<AppUser>(userStore,
new OptionsWrapper<IdentityOptions>(new IdentityOptions()),
new PasswordHasher<AppUser>(),
[new UserValidator<AppUser>()],
[new PasswordValidator<AppUser>()],
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
null!,
Substitute.For<ILogger<UserManager<AppUser>>>());
// Create users with the UserManager such that the SecurityStamp is set
await userManager.CreateAsync(user);
await userManager.CreateAsync(defaultAdmin);
var accountService = new AccountService(userManager, Substitute.For<ILogger<AccountService>>(), UnitOfWork, Mapper, Substitute.For<ILocalizationService>());
var oidcService = new OidcService(Substitute.For<ILogger<OidcService>>(), userManager, UnitOfWork, accountService, Substitute.For<IEmailService>());
return (oidcService, user, accountService, userManager);
}
protected override async Task ResetDb()
{
Context.AppUser.RemoveRange(Context.AppUser);
Context.Library.RemoveRange(Context.Library);
await UnitOfWork.CommitAsync();
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ public class SettingsServiceTests
_mockUnitOfWork = Substitute.For<IUnitOfWork>();
_settingsService = new SettingsService(_mockUnitOfWork, ds,
Substitute.For<ILibraryWatcher>(), Substitute.For<ITaskScheduler>(),
Substitute.For<ILogger<SettingsService>>());
Substitute.For<ILogger<SettingsService>>(), Substitute.For<IOidcService>());
}
#region ImportMetadataSettings