mirror of https://github.com/Squidex/squidex.git
Browse Source
* Simplified user service. * Refactor users and deleting of users. * Fix tests * Auth fix. * Should revert fixes. * Stability improvements. * Build fix. * Tests fixed. * Tests * Tests simplified.pull/624/head
committed by
GitHub
104 changed files with 1903 additions and 1260 deletions
@ -0,0 +1,408 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Security; |
|||
using Squidex.Log; |
|||
using Squidex.Shared; |
|||
using Squidex.Shared.Identity; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Users |
|||
{ |
|||
public sealed class DefaultUserService : IUserService |
|||
{ |
|||
private readonly UserManager<IdentityUser> userManager; |
|||
private readonly IUserFactory userFactory; |
|||
private readonly IEnumerable<IUserEvents> userEvents; |
|||
private readonly ISemanticLog log; |
|||
|
|||
public DefaultUserService(UserManager<IdentityUser> userManager, IUserFactory userFactory, |
|||
IEnumerable<IUserEvents> userEvents, ISemanticLog log) |
|||
{ |
|||
Guard.NotNull(userManager, nameof(userManager)); |
|||
Guard.NotNull(userFactory, nameof(userFactory)); |
|||
Guard.NotNull(userEvents, nameof(userEvents)); |
|||
Guard.NotNull(log, nameof(log)); |
|||
|
|||
this.userManager = userManager; |
|||
this.userFactory = userFactory; |
|||
this.userEvents = userEvents; |
|||
|
|||
this.log = log; |
|||
} |
|||
|
|||
public async Task<bool> IsEmptyAsync() |
|||
{ |
|||
var result = await QueryAsync(null, 0, 0); |
|||
|
|||
return result.Total == 0; |
|||
} |
|||
|
|||
public string GetUserId(ClaimsPrincipal user) |
|||
{ |
|||
Guard.NotNull(user, nameof(user)); |
|||
|
|||
return userManager.GetUserId(user); |
|||
} |
|||
|
|||
public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
|
|||
ids = ids.Where(userFactory.IsId); |
|||
|
|||
if (!ids.Any()) |
|||
{ |
|||
return ResultList.CreateFrom<IUser>(0); |
|||
} |
|||
|
|||
var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); |
|||
|
|||
var resolved = await ResolveAsync(users); |
|||
|
|||
return ResultList.Create(users.Count, resolved); |
|||
} |
|||
|
|||
public async Task<IResultList<IUser>> QueryAsync(string? query, int take, int skip) |
|||
{ |
|||
IQueryable<IdentityUser> QueryUsers(string? email = null) |
|||
{ |
|||
var result = userManager.Users; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(email)) |
|||
{ |
|||
var normalizedEmail = userManager.NormalizeEmail(email); |
|||
|
|||
result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
var userItems = QueryUsers(query).Take(take).Skip(skip).ToList(); |
|||
var userTotal = QueryUsers(query).LongCount(); |
|||
|
|||
var resolved = await ResolveAsync(userItems); |
|||
|
|||
return ResultList.Create(userTotal, resolved); |
|||
} |
|||
|
|||
public Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user) |
|||
{ |
|||
Guard.NotNull(user, nameof(user)); |
|||
|
|||
return userManager.GetLoginsAsync((IdentityUser)user.Identity); |
|||
} |
|||
|
|||
public Task<bool> HasPasswordAsync(IUser user) |
|||
{ |
|||
Guard.NotNull(user, nameof(user)); |
|||
|
|||
return userManager.HasPasswordAsync((IdentityUser)user.Identity); |
|||
} |
|||
|
|||
public async Task<IUser?> FindByLoginAsync(string provider, string key) |
|||
{ |
|||
Guard.NotNullOrEmpty(provider, nameof(provider)); |
|||
|
|||
var user = await userManager.FindByLoginAsync(provider, key); |
|||
|
|||
return await ResolveOptionalAsync(user); |
|||
} |
|||
|
|||
public async Task<IUser?> FindByEmailAsync(string email) |
|||
{ |
|||
Guard.NotNullOrEmpty(email, nameof(email)); |
|||
|
|||
var user = await userManager.FindByEmailAsync(email); |
|||
|
|||
return await ResolveOptionalAsync(user); |
|||
} |
|||
|
|||
public async Task<IUser?> GetAsync(ClaimsPrincipal principal) |
|||
{ |
|||
Guard.NotNull(principal, nameof(principal)); |
|||
|
|||
var user = await userManager.GetUserAsync(principal); |
|||
|
|||
return await ResolveOptionalAsync(user); |
|||
} |
|||
|
|||
public async Task<IUser?> FindByIdAsync(string id) |
|||
{ |
|||
if (!userFactory.IsId(id)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var user = await userManager.FindByIdAsync(id); |
|||
|
|||
return await ResolveOptionalAsync(user); |
|||
} |
|||
|
|||
public async Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false) |
|||
{ |
|||
Guard.NotNullOrEmpty(email, nameof(email)); |
|||
|
|||
var isFirst = !userManager.Users.Any(); |
|||
|
|||
var user = userFactory.Create(email); |
|||
|
|||
try |
|||
{ |
|||
await userManager.CreateAsync(user).Throw(log); |
|||
|
|||
values ??= new UserValues(); |
|||
|
|||
if (string.IsNullOrWhiteSpace(values.DisplayName)) |
|||
{ |
|||
values.DisplayName = email; |
|||
} |
|||
|
|||
if (isFirst) |
|||
{ |
|||
var permissions = values.Permissions?.ToIds().ToList() ?? new List<string>(); |
|||
|
|||
permissions.Add(Permissions.Admin); |
|||
|
|||
values.Permissions = new PermissionSet(permissions); |
|||
} |
|||
|
|||
await userManager.SyncClaims(user, values).Throw(log); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(values.Password)) |
|||
{ |
|||
await userManager.AddPasswordAsync(user, values.Password).Throw(log); |
|||
} |
|||
|
|||
if (!isFirst && lockAutomatically) |
|||
{ |
|||
await userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log); |
|||
} |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
try |
|||
{ |
|||
if (userFactory.IsId(user.Id)) |
|||
{ |
|||
await userManager.DeleteAsync(user); |
|||
} |
|||
} |
|||
catch (Exception ex2) |
|||
{ |
|||
log.LogError(ex2, w => w |
|||
.WriteProperty("action", "CleanupUser") |
|||
.WriteProperty("status", "Failed")); |
|||
} |
|||
|
|||
throw; |
|||
} |
|||
|
|||
var resolved = await ResolveAsync(user); |
|||
|
|||
foreach (var @events in userEvents) |
|||
{ |
|||
@events.OnUserRegistered(resolved); |
|||
} |
|||
|
|||
if (HasConsentGiven(values, null!)) |
|||
{ |
|||
foreach (var @events in userEvents) |
|||
{ |
|||
@events.OnConsentGiven(resolved); |
|||
} |
|||
} |
|||
|
|||
return resolved; |
|||
} |
|||
|
|||
public Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
return ForUserAsync(id, async user => |
|||
{ |
|||
if (await userManager.HasPasswordAsync(user)) |
|||
{ |
|||
await userManager.ChangePasswordAsync(user, oldPassword!, password).Throw(log); |
|||
} |
|||
else |
|||
{ |
|||
await userManager.AddPasswordAsync(user, password).Throw(log); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public async Task<IUser> UpdateAsync(string id, UserValues values) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
Guard.NotNull(values, nameof(values)); |
|||
|
|||
var user = await GetUserAsync(id); |
|||
|
|||
var oldUser = await ResolveAsync(user); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) |
|||
{ |
|||
await userManager.SetEmailAsync(user, values.Email).Throw(log); |
|||
await userManager.SetUserNameAsync(user, values.Email).Throw(log); |
|||
} |
|||
|
|||
await userManager.SyncClaims(user, values).Throw(log); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(values.Password)) |
|||
{ |
|||
if (await userManager.HasPasswordAsync(user)) |
|||
{ |
|||
await userManager.RemovePasswordAsync(user).Throw(log); |
|||
} |
|||
|
|||
await userManager.AddPasswordAsync(user, values.Password).Throw(log); |
|||
} |
|||
|
|||
var resolved = await ResolveAsync(user); |
|||
|
|||
foreach (var @events in userEvents) |
|||
{ |
|||
@events.OnUserUpdated(resolved); |
|||
} |
|||
|
|||
if (HasConsentGiven(values, oldUser)) |
|||
{ |
|||
foreach (var @events in userEvents) |
|||
{ |
|||
@events.OnConsentGiven(resolved); |
|||
} |
|||
} |
|||
|
|||
return resolved; |
|||
} |
|||
|
|||
public Task<IUser> LockAsync(string id) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); |
|||
} |
|||
|
|||
public Task<IUser> UnlockAsync(string id) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); |
|||
} |
|||
|
|||
public Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); |
|||
} |
|||
|
|||
public Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); |
|||
} |
|||
|
|||
public async Task DeleteAsync(string id) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
var user = await GetUserAsync(id); |
|||
|
|||
var resolved = await ResolveAsync(user); |
|||
|
|||
await userManager.DeleteAsync(user).Throw(log); |
|||
|
|||
foreach (var @events in userEvents) |
|||
{ |
|||
@events.OnUserDeleted(resolved); |
|||
} |
|||
} |
|||
|
|||
private async Task<IUser> ForUserAsync(string id, Func<IdentityUser, Task> action) |
|||
{ |
|||
var user = await GetUserAsync(id); |
|||
|
|||
await action(user); |
|||
|
|||
return await ResolveAsync(user); |
|||
} |
|||
|
|||
private async Task<IdentityUser> GetUserAsync(string id) |
|||
{ |
|||
if (!userFactory.IsId(id)) |
|||
{ |
|||
throw new DomainObjectNotFoundException(id); |
|||
} |
|||
|
|||
var user = await userManager.FindByIdAsync(id); |
|||
|
|||
if (user == null) |
|||
{ |
|||
throw new DomainObjectNotFoundException(id); |
|||
} |
|||
|
|||
return user; |
|||
} |
|||
|
|||
private Task<IUser[]> ResolveAsync(IEnumerable<IdentityUser> users) |
|||
{ |
|||
return Task.WhenAll(users.Select(async user => |
|||
{ |
|||
return await ResolveAsync(user); |
|||
})); |
|||
} |
|||
|
|||
private async Task<IUser> ResolveAsync(IdentityUser user) |
|||
{ |
|||
var claims = await userManager.GetClaimsAsync(user); |
|||
|
|||
if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) |
|||
{ |
|||
claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); |
|||
} |
|||
|
|||
return new UserWithClaims(user, claims.ToList()); |
|||
} |
|||
|
|||
private async Task<IUser?> ResolveOptionalAsync(IdentityUser? user) |
|||
{ |
|||
if (user == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return await ResolveAsync(user); |
|||
} |
|||
|
|||
private static bool HasConsentGiven(UserValues values, IUser? oldUser) |
|||
{ |
|||
if (values.Consent == true && oldUser?.Claims.HasConsent() != true) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return values.ConsentForEmails == true && oldUser?.Claims.HasConsentForEmails() != true; |
|||
} |
|||
|
|||
private static DateTimeOffset LockoutDate() |
|||
{ |
|||
return DateTimeOffset.UtcNow.AddYears(100); |
|||
} |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Users |
|||
{ |
|||
public interface IUserEventHandler |
|||
{ |
|||
void OnUserRegistered(IUser user) |
|||
{ |
|||
} |
|||
|
|||
void OnUserUpdated(IUser user) |
|||
{ |
|||
} |
|||
|
|||
void OnConsentGiven(IUser user) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Users |
|||
{ |
|||
public interface IUserService |
|||
{ |
|||
Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids); |
|||
|
|||
Task<IResultList<IUser>> QueryAsync(string? query = null, int take = 10, int skip = 0); |
|||
|
|||
string GetUserId(ClaimsPrincipal user); |
|||
|
|||
Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user); |
|||
|
|||
Task<bool> HasPasswordAsync(IUser user); |
|||
|
|||
Task<bool> IsEmptyAsync(); |
|||
|
|||
Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false); |
|||
|
|||
Task<IUser?> GetAsync(ClaimsPrincipal principal); |
|||
|
|||
Task<IUser?> FindByEmailAsync(string email); |
|||
|
|||
Task<IUser?> FindByIdAsync(string id); |
|||
|
|||
Task<IUser?> FindByLoginAsync(string provider, string key); |
|||
|
|||
Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword = null); |
|||
|
|||
Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin); |
|||
|
|||
Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey); |
|||
|
|||
Task<IUser> LockAsync(string id); |
|||
|
|||
Task<IUser> UnlockAsync(string id); |
|||
|
|||
Task<IUser> UpdateAsync(string id, UserValues values); |
|||
|
|||
Task DeleteAsync(string id); |
|||
} |
|||
} |
|||
@ -1,49 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Users |
|||
{ |
|||
public sealed class UserEvents : IUserEvents |
|||
{ |
|||
private readonly IEnumerable<IUserEventHandler> userEventHandlers; |
|||
|
|||
public UserEvents(IEnumerable<IUserEventHandler> userEventHandlers) |
|||
{ |
|||
Guard.NotNull(userEventHandlers, nameof(userEventHandlers)); |
|||
|
|||
this.userEventHandlers = userEventHandlers; |
|||
} |
|||
|
|||
public void OnUserRegistered(IUser user) |
|||
{ |
|||
foreach (var handler in userEventHandlers) |
|||
{ |
|||
handler.OnUserRegistered(user); |
|||
} |
|||
} |
|||
|
|||
public void OnUserUpdated(IUser user) |
|||
{ |
|||
foreach (var handler in userEventHandlers) |
|||
{ |
|||
handler.OnUserUpdated(user); |
|||
} |
|||
} |
|||
|
|||
public void OnConsentGiven(IUser user) |
|||
{ |
|||
foreach (var handler in userEventHandlers) |
|||
{ |
|||
handler.OnConsentGiven(user); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using Squidex.Infrastructure.Security; |
|||
|
|||
namespace Squidex.Shared.Identity |
|||
{ |
|||
public static class ClaimsPrincipalExtensions |
|||
{ |
|||
public static void SetDisplayName(this ClaimsIdentity identity, string displayName) |
|||
{ |
|||
identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName)); |
|||
} |
|||
|
|||
public static void SetPictureUrl(this ClaimsIdentity identity, string pictureUrl) |
|||
{ |
|||
identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); |
|||
} |
|||
|
|||
public static PermissionSet Permissions(this ClaimsPrincipal principal) |
|||
{ |
|||
return new PermissionSet(principal.Claims |
|||
.Where(x => |
|||
(x.Type == SquidexClaimTypes.Permissions || |
|||
x.Type == SquidexClaimTypes.PermissionsClient) && |
|||
!string.IsNullOrWhiteSpace(x.Value)) |
|||
.Select(x => new Permission(x.Value))); |
|||
} |
|||
|
|||
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal) |
|||
{ |
|||
return principal.Claims |
|||
.Where(x => |
|||
(x.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal) || |
|||
x.Type.StartsWith(SquidexClaimTypes.PrefixClient, StringComparison.Ordinal)) && |
|||
!string.IsNullOrWhiteSpace(x.Value)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using Squidex.Infrastructure.Security; |
|||
|
|||
namespace Squidex.Shared.Identity |
|||
{ |
|||
public static class SquidexClaimsExtensions |
|||
{ |
|||
private const string ClientPrefix = "client_"; |
|||
|
|||
public static PermissionSet Permissions(this IEnumerable<Claim> user) |
|||
{ |
|||
return new PermissionSet(user.GetClaims(SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value))); |
|||
} |
|||
|
|||
public static bool IsHidden(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); |
|||
} |
|||
|
|||
public static bool HasConsent(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); |
|||
} |
|||
|
|||
public static bool HasConsentForEmails(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); |
|||
} |
|||
|
|||
public static bool HasDisplayName(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.HasClaim(SquidexClaimTypes.DisplayName); |
|||
} |
|||
|
|||
public static bool HasPictureUrl(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.HasClaim(SquidexClaimTypes.PictureUrl); |
|||
} |
|||
|
|||
public static bool IsPictureUrlStored(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); |
|||
} |
|||
|
|||
public static string? ClientSecret(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.GetClaimValue(SquidexClaimTypes.ClientSecret); |
|||
} |
|||
|
|||
public static string? PictureUrl(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.GetClaimValue(SquidexClaimTypes.PictureUrl); |
|||
} |
|||
|
|||
public static string? DisplayName(this IEnumerable<Claim> user) |
|||
{ |
|||
return user.GetClaimValue(SquidexClaimTypes.DisplayName); |
|||
} |
|||
|
|||
public static bool HasClaim(this IEnumerable<Claim> user, string type) |
|||
{ |
|||
return user.GetClaims(type).Any(); |
|||
} |
|||
|
|||
public static bool HasClaimValue(this IEnumerable<Claim> user, string type, string value) |
|||
{ |
|||
return user.GetClaims(type).Any(x => string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); |
|||
} |
|||
|
|||
public static IEnumerable<Claim> GetSquidexClaims(this IEnumerable<Claim> user) |
|||
{ |
|||
const string prefix = "urn:squidex:"; |
|||
|
|||
foreach (var claim in user) |
|||
{ |
|||
var type = GetType(claim); |
|||
|
|||
if (type.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
yield return claim; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static IEnumerable<(string Name, string Value)> GetCustomProperties(this IEnumerable<Claim> user) |
|||
{ |
|||
foreach (var claim in user) |
|||
{ |
|||
var type = GetType(claim); |
|||
|
|||
if (type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var name = type[(SquidexClaimTypes.CustomPrefix.Length + 1)..].ToString(); |
|||
|
|||
yield return (name, claim.Value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static string? PictureNormalizedUrl(this IEnumerable<Claim> user) |
|||
{ |
|||
var url = user.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) |
|||
{ |
|||
if (url.Contains("?")) |
|||
{ |
|||
url += "&d=404"; |
|||
} |
|||
else |
|||
{ |
|||
url += "?d=404"; |
|||
} |
|||
} |
|||
|
|||
return url; |
|||
} |
|||
|
|||
private static string? GetClaimValue(this IEnumerable<Claim> user, string type) |
|||
{ |
|||
return user.GetClaims(type).FirstOrDefault()?.Value; |
|||
} |
|||
|
|||
private static IEnumerable<Claim> GetClaims(this IEnumerable<Claim> user, string request) |
|||
{ |
|||
foreach (var claim in user) |
|||
{ |
|||
var type = GetType(claim); |
|||
|
|||
if (type.Equals(request, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
yield return claim; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static ReadOnlySpan<char> GetType(Claim claim) |
|||
{ |
|||
var type = claim.Type.AsSpan(); |
|||
|
|||
if (type.StartsWith(ClientPrefix, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
type = type[ClientPrefix.Length..]; |
|||
} |
|||
|
|||
return type; |
|||
} |
|||
} |
|||
} |
|||
@ -1,121 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure.Security; |
|||
using Squidex.Shared.Identity; |
|||
|
|||
namespace Squidex.Shared.Users |
|||
{ |
|||
public static class UserExtensions |
|||
{ |
|||
public static PermissionSet Permissions(this IUser user) |
|||
{ |
|||
return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); |
|||
} |
|||
|
|||
public static bool IsInvited(this IUser user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); |
|||
} |
|||
|
|||
public static bool IsHidden(this IUser user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); |
|||
} |
|||
|
|||
public static bool HasConsent(this IUser user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); |
|||
} |
|||
|
|||
public static bool HasConsentForEmails(this IUser user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); |
|||
} |
|||
|
|||
public static bool HasDisplayName(this IUser user) |
|||
{ |
|||
return user.HasClaim(SquidexClaimTypes.DisplayName); |
|||
} |
|||
|
|||
public static bool HasPictureUrl(this IUser user) |
|||
{ |
|||
return user.HasClaim(SquidexClaimTypes.PictureUrl); |
|||
} |
|||
|
|||
public static bool IsPictureUrlStored(this IUser user) |
|||
{ |
|||
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); |
|||
} |
|||
|
|||
public static string? ClientSecret(this IUser user) |
|||
{ |
|||
return user.GetClaimValue(SquidexClaimTypes.ClientSecret); |
|||
} |
|||
|
|||
public static string? PictureUrl(this IUser user) |
|||
{ |
|||
return user.GetClaimValue(SquidexClaimTypes.PictureUrl); |
|||
} |
|||
|
|||
public static string? DisplayName(this IUser user) |
|||
{ |
|||
return user.GetClaimValue(SquidexClaimTypes.DisplayName); |
|||
} |
|||
|
|||
public static string? GetClaimValue(this IUser user, string type) |
|||
{ |
|||
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; |
|||
} |
|||
|
|||
public static string[] GetClaimValues(this IUser user, string type) |
|||
{ |
|||
return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)) |
|||
.Select(x => x.Value).ToArray(); |
|||
} |
|||
|
|||
public static List<(string Name, string Value)> GetCustomProperties(this IUser user) |
|||
{ |
|||
return user.Claims.Where(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)) |
|||
.Select(x => (x.Type[(SquidexClaimTypes.CustomPrefix.Length + 1)..], x.Value)).ToList(); |
|||
} |
|||
|
|||
public static bool HasClaim(this IUser user, string type) |
|||
{ |
|||
return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); |
|||
} |
|||
|
|||
public static bool HasClaimValue(this IUser user, string type, string value) |
|||
{ |
|||
return user.Claims.Any(x => |
|||
string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && |
|||
string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); |
|||
} |
|||
|
|||
public static string? PictureNormalizedUrl(this IUser user) |
|||
{ |
|||
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) |
|||
{ |
|||
if (url.Contains("?")) |
|||
{ |
|||
url += "&d=404"; |
|||
} |
|||
else |
|||
{ |
|||
url += "?d=404"; |
|||
} |
|||
} |
|||
|
|||
return url; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,607 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Log; |
|||
using Squidex.Shared; |
|||
using Squidex.Shared.Identity; |
|||
using Squidex.Shared.Users; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Users |
|||
{ |
|||
public class DefaultUserServiceTests |
|||
{ |
|||
private readonly UserManager<IdentityUser> userManager = A.Fake<UserManager<IdentityUser>>(); |
|||
private readonly IUserFactory userFactory = A.Fake<IUserFactory>(); |
|||
private readonly IUserEvents userEvents = A.Fake<IUserEvents>(); |
|||
private readonly DefaultUserService sut; |
|||
|
|||
public DefaultUserServiceTests() |
|||
{ |
|||
A.CallTo(() => userFactory.IsId(A<string>._)) |
|||
.Returns(true); |
|||
|
|||
A.CallTo(userManager).WithReturnType<Task<IdentityResult>>() |
|||
.Returns(IdentityResult.Success); |
|||
|
|||
sut = new DefaultUserService(userManager, userFactory, Enumerable.Repeat(userEvents, 1), A.Fake<ISemanticLog>()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_resolve_identity_if_id_not_valid() |
|||
{ |
|||
var invalidId = "__"; |
|||
|
|||
A.CallTo(() => userFactory.IsId(invalidId)) |
|||
.Returns(false); |
|||
|
|||
var result = await sut.FindByIdAsync(invalidId); |
|||
|
|||
Assert.Null(result); |
|||
|
|||
A.CallTo(() => userManager.FindByIdAsync(invalidId)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_identity_by_id_if_found() |
|||
{ |
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
var result = await sut.FindByIdAsync(identity.Id); |
|||
|
|||
Assert.Same(identity, result?.Identity); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_null_if_identity_by_id_not_found() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var result = await sut.FindByIdAsync(identity.Id); |
|||
|
|||
Assert.Null(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_identity_by_email_if_found() |
|||
{ |
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
var result = await sut.FindByEmailAsync(identity.Email); |
|||
|
|||
Assert.Same(identity, result?.Identity); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_null_if_identity_by_email_not_found() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var result = await sut.FindByEmailAsync(identity.Email); |
|||
|
|||
Assert.Null(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_identity_by_login_if_found() |
|||
{ |
|||
var provider = "my-provider"; |
|||
var providerKey = "key"; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) |
|||
.Returns(identity); |
|||
|
|||
var result = await sut.FindByLoginAsync(provider, providerKey); |
|||
|
|||
Assert.Same(identity, result?.Identity); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_null_if_identity_by_login_not_found() |
|||
{ |
|||
var provider = "my-provider"; |
|||
var providerKey = "key"; |
|||
|
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) |
|||
.Returns(Task.FromResult<IdentityUser>(null!)); |
|||
|
|||
var result = await sut.FindByLoginAsync(provider, providerKey); |
|||
|
|||
Assert.Null(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_provide_password_existence() |
|||
{ |
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
var user = A.Fake<IUser>(); |
|||
|
|||
A.CallTo(() => user.Identity) |
|||
.Returns(identity); |
|||
|
|||
A.CallTo(() => userManager.HasPasswordAsync(identity)) |
|||
.Returns(true); |
|||
|
|||
var result = await sut.HasPasswordAsync(user); |
|||
|
|||
Assert.True(result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_provide_logins() |
|||
{ |
|||
var logins = new List<UserLoginInfo>(); |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
var user = A.Fake<IUser>(); |
|||
|
|||
A.CallTo(() => user.Identity) |
|||
.Returns(identity); |
|||
|
|||
A.CallTo(() => userManager.GetLoginsAsync(identity)) |
|||
.Returns(logins); |
|||
|
|||
var result = await sut.GetLoginsAsync(user); |
|||
|
|||
Assert.Same(logins, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_add_user() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var values = new UserValues |
|||
{ |
|||
Email = identity.Email |
|||
}; |
|||
|
|||
SetupCreation(identity, values, 1); |
|||
|
|||
await sut.CreateAsync(values.Email, values); |
|||
|
|||
A.CallTo(() => userEvents.OnUserRegistered(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions))) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => userManager.AddPasswordAsync(identity, A<string>._)) |
|||
.MustNotHaveHappened(); |
|||
|
|||
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_raise_event_if_consent_given() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var values = new UserValues |
|||
{ |
|||
Consent = true, |
|||
}; |
|||
|
|||
SetupCreation(identity, values, 1); |
|||
|
|||
await sut.CreateAsync(identity.Email, values); |
|||
|
|||
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_set_admin_if_first_user() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var values = new UserValues |
|||
{ |
|||
Consent = true, |
|||
}; |
|||
|
|||
SetupCreation(identity, values, 0); |
|||
|
|||
await sut.CreateAsync(identity.Email, values); |
|||
|
|||
A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions, Permissions.Admin))) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_not_lock_first_user() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var values = new UserValues |
|||
{ |
|||
Consent = true, |
|||
}; |
|||
|
|||
SetupCreation(identity, values, 0); |
|||
|
|||
await sut.CreateAsync(identity.Email, values, true); |
|||
|
|||
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_lock_second_user() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var values = new UserValues |
|||
{ |
|||
Consent = true, |
|||
}; |
|||
|
|||
SetupCreation(identity, values, 1); |
|||
|
|||
await sut.CreateAsync(identity.Email, values, true); |
|||
|
|||
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_add_password() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
var values = new UserValues |
|||
{ |
|||
Password = "password" |
|||
}; |
|||
|
|||
SetupCreation(identity, values, 1); |
|||
|
|||
await sut.CreateAsync(identity.Email, values, false); |
|||
|
|||
A.CallTo(() => userManager.AddPasswordAsync(identity, values.Password)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_throw_exception_if_not_found() |
|||
{ |
|||
var update = new UserValues |
|||
{ |
|||
Email = "new@email.com" |
|||
}; |
|||
|
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UpdateAsync(identity.Id, update)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_do_nothing_for_new_update() |
|||
{ |
|||
var update = new UserValues(); |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.UpdateAsync(identity.Id, update); |
|||
|
|||
A.CallTo(() => userEvents.OnUserUpdated(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_change_password_if_changed() |
|||
{ |
|||
var update = new UserValues |
|||
{ |
|||
Password = "password" |
|||
}; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
A.CallTo(() => userManager.HasPasswordAsync(identity)) |
|||
.Returns(true); |
|||
|
|||
await sut.UpdateAsync(identity.Id, update); |
|||
|
|||
A.CallTo(() => userManager.RemovePasswordAsync(identity)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => userManager.AddPasswordAsync(identity, update.Password)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_change_email_if_changed() |
|||
{ |
|||
var update = new UserValues |
|||
{ |
|||
Email = "new@email.com" |
|||
}; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.UpdateAsync(identity.Id, update); |
|||
|
|||
A.CallTo(() => userManager.SetEmailAsync(identity, update.Email)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => userManager.SetUserNameAsync(identity, update.Email)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_set_claim_if_consent_given() |
|||
{ |
|||
var update = new UserValues |
|||
{ |
|||
Consent = true |
|||
}; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.UpdateAsync(identity.Id, update); |
|||
|
|||
A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Consent))) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_set_claim_if_email_consent_given() |
|||
{ |
|||
var update = new UserValues |
|||
{ |
|||
ConsentForEmails = true |
|||
}; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.UpdateAsync(identity.Id, update); |
|||
|
|||
A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.ConsentForEmails))) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SetPassword_should_throw_exception_if_not_found() |
|||
{ |
|||
var password = "password"; |
|||
|
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.SetPasswordAsync(identity.Id, password, null)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SetPassword_should_succeed_if_found() |
|||
{ |
|||
var password = "password"; |
|||
|
|||
var identity = CreateIdentity(found: true);; |
|||
|
|||
await sut.SetPasswordAsync(identity.Id, password, null); |
|||
|
|||
A.CallTo(() => userManager.AddPasswordAsync(identity, password)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SetPassword_should_change_password_if_identity_has_password() |
|||
{ |
|||
var password = "password"; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
A.CallTo(() => userManager.HasPasswordAsync(identity)) |
|||
.Returns(true); |
|||
|
|||
await sut.SetPasswordAsync(identity.Id, password, "old"); |
|||
|
|||
A.CallTo(() => userManager.ChangePasswordAsync(identity, "old", password)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AddLogin_should_throw_exception_if_not_found() |
|||
{ |
|||
var login = A.Fake<ExternalLoginInfo>(); |
|||
|
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.AddLoginAsync(identity.Id, login)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AddLogin_should_succeed_if_found() |
|||
{ |
|||
var login = A.Fake<ExternalLoginInfo>(); |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.AddLoginAsync(identity.Id, login); |
|||
|
|||
A.CallTo(() => userManager.AddLoginAsync(identity, login)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task RemoveLogin_should_throw_exception_if_not_found() |
|||
{ |
|||
var provider = "my-provider"; |
|||
var providerKey = "key"; |
|||
|
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RemoveLoginAsync(identity.Id, provider, providerKey)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task RemoveLogin_should_succeed_if_found() |
|||
{ |
|||
var provider = "my-provider"; |
|||
var providerKey = "key"; |
|||
|
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.RemoveLoginAsync(identity.Id, provider, providerKey); |
|||
|
|||
A.CallTo(() => userManager.RemoveLoginAsync(identity, provider, providerKey)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Lock_should_throw_exception_if_not_found() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LockAsync(identity.Id)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Lock_should_succeed_if_found() |
|||
{ |
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.LockAsync(identity.Id); |
|||
|
|||
A.CallTo<Task<IdentityResult>>(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Unlock_should_throw_exception_if_not_found() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UnlockAsync(identity.Id)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Unlock_should_succeeed_if_found() |
|||
{ |
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.UnlockAsync(identity.Id); |
|||
|
|||
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, null)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Delete_should_throw_exception_if_not_found() |
|||
{ |
|||
var identity = CreateIdentity(found: false); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.DeleteAsync(identity.Id)); |
|||
|
|||
A.CallTo(() => userEvents.OnUserDeleted(A<IUser>._)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Delete_should_succeed_if_found() |
|||
{ |
|||
var identity = CreateIdentity(found: true); |
|||
|
|||
await sut.DeleteAsync(identity.Id); |
|||
|
|||
A.CallTo(() => userManager.DeleteAsync(identity)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => userEvents.OnUserDeleted(A<IUser>.That.Matches(x => x.Identity == identity))) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
private IdentityUser CreateIdentity(bool found, string id = "123") |
|||
{ |
|||
var identity = CreatePendingUser(id); |
|||
|
|||
if (found) |
|||
{ |
|||
A.CallTo(() => userManager.FindByIdAsync(identity.Id)) |
|||
.Returns(identity); |
|||
|
|||
A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) |
|||
.Returns(identity); |
|||
} |
|||
else |
|||
{ |
|||
A.CallTo(() => userManager.FindByIdAsync(identity.Id)) |
|||
.Returns(Task.FromResult<IdentityUser>(null!)); |
|||
|
|||
A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) |
|||
.Returns(Task.FromResult<IdentityUser>(null!)); |
|||
} |
|||
|
|||
return identity; |
|||
} |
|||
|
|||
private void SetupCreation(IdentityUser identity, UserValues values, int numCurrentUsers) |
|||
{ |
|||
var users = new List<IdentityUser>(); |
|||
|
|||
for (var i = 0; i < numCurrentUsers; i++) |
|||
{ |
|||
users.Add(CreatePendingUser(i.ToString())); |
|||
} |
|||
|
|||
A.CallTo(() => userManager.Users) |
|||
.Returns(users.AsQueryable()); |
|||
|
|||
A.CallTo(() => userFactory.Create(identity.Email)) |
|||
.Returns(identity); |
|||
} |
|||
|
|||
private static IEnumerable<Claim> HasClaim(string claim) |
|||
{ |
|||
return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim)); |
|||
} |
|||
|
|||
private static IEnumerable<Claim> HasClaim(string claim, string value) |
|||
{ |
|||
return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim && y.Value == value)); |
|||
} |
|||
|
|||
private static DateTimeOffset InFuture() |
|||
{ |
|||
return A<DateTimeOffset>.That.Matches(x => x >= DateTimeOffset.UtcNow.AddYears(1)); |
|||
} |
|||
|
|||
private static IdentityUser CreatePendingUser(string id = "123") |
|||
{ |
|||
return new IdentityUser |
|||
{ |
|||
Id = id, |
|||
Email = $"{id}@email.com" |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue