// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Globalization; using System.Security.Claims; using FakeItEasy; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Squidex.Infrastructure; using Squidex.Shared; using Squidex.Shared.Identity; using Squidex.Shared.Users; using Xunit; namespace Squidex.Domain.Users { public class DefaultUserServiceTests { private readonly UserManager userManager = A.Fake>(); private readonly IUserFactory userFactory = A.Fake(); private readonly IUserEvents userEvents = A.Fake(); private readonly DefaultUserService sut; public DefaultUserServiceTests() { A.CallTo(() => userFactory.IsId(A._)) .Returns(true); A.CallTo(userManager).WithReturnType>() .Returns(IdentityResult.Success); var log = A.Fake>(); sut = new DefaultUserService(userManager, userFactory, Enumerable.Repeat(userEvents, 1), log); } [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"; CreateIdentity(found: false); A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) .Returns(Task.FromResult(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(); 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(); var identity = CreateIdentity(found: true); var user = A.Fake(); 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, 1); await sut.CreateAsync(values.Email, values); A.CallTo(() => userEvents.OnUserRegisteredAsync(A.That.Matches(x => x.Identity == identity))) .MustHaveHappened(); A.CallTo(() => userEvents.OnConsentGivenAsync(A.That.Matches(x => x.Identity == identity))) .MustNotHaveHappened(); A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions))) .MustNotHaveHappened(); A.CallTo(() => userManager.AddPasswordAsync(identity, A._)) .MustNotHaveHappened(); A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A._)) .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, 1); await sut.CreateAsync(identity.Email, values); A.CallTo(() => userEvents.OnConsentGivenAsync(A.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, 0); await sut.CreateAsync(identity.Email, values); A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions, PermissionIds.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, 0); await sut.CreateAsync(identity.Email, values, true); A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A._)) .MustNotHaveHappened(); } [Fact] public async Task Create_should_lock_second_user() { var identity = CreateIdentity(found: false); var values = new UserValues { Consent = true }; SetupCreation(identity, 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, 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(() => sut.UpdateAsync(identity.Id, update)); } [Fact] public async Task Update_should_not_invoke_events_if_silent() { var update = new UserValues(); var identity = CreateIdentity(found: true); await sut.UpdateAsync(identity.Id, update, true); A.CallTo(() => userEvents.OnUserUpdatedAsync(A.That.Matches(x => x.Identity == identity), A._)) .MustNotHaveHappened(); } [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.OnUserUpdatedAsync(A.That.Matches(x => x.Identity == identity), A._)) .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>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Consent))) .MustHaveHappened(); A.CallTo(() => userEvents.OnConsentGivenAsync(A.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>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.ConsentForEmails))) .MustHaveHappened(); A.CallTo(() => userEvents.OnConsentGivenAsync(A.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(() => 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(); var identity = CreateIdentity(found: false); await Assert.ThrowsAsync(() => sut.AddLoginAsync(identity.Id, login)); } [Fact] public async Task AddLogin_should_succeed_if_found() { var login = A.Fake(); 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(() => 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(() => 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>(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) .MustHaveHappened(); } [Fact] public async Task Unlock_should_throw_exception_if_not_found() { var identity = CreateIdentity(found: false); await Assert.ThrowsAsync(() => 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(() => sut.DeleteAsync(identity.Id)); A.CallTo(() => userEvents.OnUserDeletedAsync(A._)) .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.OnUserDeletedAsync(A.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(null!)); A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) .Returns(Task.FromResult(null!)); } return identity; } private void SetupCreation(IdentityUser identity, int numCurrentUsers) { var users = new List(); for (var i = 0; i < numCurrentUsers; i++) { users.Add(CreatePendingUser(i.ToString(CultureInfo.InvariantCulture))); } A.CallTo(() => userManager.Users) .Returns(users.AsQueryable()); A.CallTo(() => userFactory.Create(identity.Email)) .Returns(identity); } private static IEnumerable HasClaim(string claim) { return A>.That.Matches(x => x.Any(y => y.Type == claim)); } private static IEnumerable HasClaim(string claim, string value) { return A>.That.Matches(x => x.Any(y => y.Type == claim && y.Value == value)); } private static DateTimeOffset InFuture() { return A.That.Matches(x => x >= DateTimeOffset.UtcNow.AddYears(1)); } private static IdentityUser CreatePendingUser(string id = "123") { return new IdentityUser { Id = id, Email = $"{id}@email.com" }; } } }