mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
A token carries the user's roles and account, so a demoted admin kept admin claims until the token expired. The team member update and the admin user edit now rotate the security stamp and evict the cached value when the role, account or user name changes. Permission overrides are read per request and are not in the token.
472 lines
20 KiB
C#
472 lines
20 KiB
C#
using Data.SeaHavenIndustries;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Moq;
|
|
using SeaHaven.DataServices.Dto;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using System.Security.Claims;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public class UserServiceTests
|
|
{
|
|
private static ClaimsPrincipal Principal(params string[] roles)
|
|
{
|
|
var claims = roles.Select(r => new Claim(ClaimTypes.Role, r)).ToList();
|
|
claims.Add(new Claim(ClaimTypes.NameIdentifier, "caller-1"));
|
|
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"));
|
|
}
|
|
|
|
private static UserService NewService(
|
|
Mock<IUserDataService> userData,
|
|
Mock<IEmailSender> email,
|
|
out Mock<IUserRoleStore<ApplicationUser>> store,
|
|
Mock<IAccountDataService>? accounts = null,
|
|
Mock<ISessionStampService>? sessions = null)
|
|
{
|
|
var (manager, s, _) = IdentityTestHelpers.CreateUserManager();
|
|
store = s;
|
|
return new UserService(
|
|
manager,
|
|
userData.Object,
|
|
(accounts ?? new Mock<IAccountDataService>()).Object,
|
|
email.Object,
|
|
(sessions ?? new Mock<ISessionStampService>()).Object);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUsers_FormatsCreatedDate_AndHandlesNullDate()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetAllForListAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<UserListRowData>
|
|
{
|
|
new() { Id = "1", Name = "A", Email = "a@x.com", RoleName = "Admin", Status = "Active", CreatedDate = new DateTime(2026, 1, 5) },
|
|
new() { Id = "2", Name = "B", Email = "b@x.com", RoleName = "Tech", Status = "Active", CreatedDate = null }
|
|
});
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var users = (await service.GetUsersAsync(CancellationToken.None)).ToList();
|
|
|
|
users.Should().HaveCount(2);
|
|
users[0].Date.Should().Be(new DateTime(2026, 1, 5).ToString("MMM dd yyyy"));
|
|
users[0].RoleName.Should().Be("Admin");
|
|
users[0].Status.Should().Be("Active");
|
|
users[1].Date.Should().Be("");
|
|
users[1].Name.Should().Be("B");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteUser_NotFound_ReturnsUserNotFoundWithoutCascade()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("missing", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var outcome = await service.DeleteUserAsync("missing", Principal("Admin"), CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.UserNotFound);
|
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteUser_Found_PerformsCascade()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var outcome = await service.DeleteUserAsync("u1", Principal("Admin"), CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeTrue();
|
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteUser_AccountOwner_ReturnsForbiddenWithoutCascade()
|
|
{
|
|
var user = IdentityTestHelpers.User("owner");
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("owner", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
userData.Setup(u => u.IsAccountOwnerAsync("owner", It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var outcome = await service.DeleteUserAsync("owner", Principal("Admin"), CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteUser_NonAdmin_ReturnsForbiddenWithoutCascade()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var outcome = await service.DeleteUserAsync("u1", Principal("User"), CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteCurrentUser_SetsIsDeletedFlag()
|
|
{
|
|
var user = IdentityTestHelpers.User();
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
await service.DeleteCurrentUserAsync("u1", CancellationToken.None);
|
|
|
|
user.IsDeleted.Should().BeTrue();
|
|
userData.Verify(u => u.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditUser_NotFound_ReturnsUserNotFound()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("x", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var outcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO { Id = "x", Email = "e@x.com", Name = "n", Role = "Admin" },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.UserNotFound);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditUser_AccountOwner_ReturnsForbiddenWithoutMutation()
|
|
{
|
|
var user = IdentityTestHelpers.User("owner");
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("owner", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
userData.Setup(u => u.IsAccountOwnerAsync("owner", It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var outcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO { Id = "owner", Email = "owner@example.com", Name = "Owner", Role = "User" },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
|
userData.Verify(u => u.UpdateUserAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUserProfile_MapsCreatedDateToAddedDate()
|
|
{
|
|
var user = new UserProfileData
|
|
{
|
|
Id = "u1",
|
|
FirstName = "Alice",
|
|
CreatedDate = new DateTime(2026, 3, 4)
|
|
};
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetProfileAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
|
|
|
var rows = await service.GetUserProfileAsync("u1", CancellationToken.None);
|
|
|
|
rows.Should().HaveCount(1);
|
|
rows[0].AddedDate.Should().Be(new DateTime(2026, 3, 4));
|
|
rows[0].Name.Should().Be("Alice");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddUser_CreateSuccess_PersistsRoleAndEmailsLoginInformation()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByIdAsync(It.IsAny<string>())).ReturnsAsync((ApplicationUser?)null);
|
|
var email = new Mock<IEmailSender>();
|
|
|
|
var service = NewService(userData, email, out var store);
|
|
store.As<IUserPasswordStore<ApplicationUser>>()
|
|
.Setup(s => s.SetPasswordHashAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
store.Setup(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(IdentityResult.Success);
|
|
store.Setup(s => s.AddToRoleAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var outcome = await service.AddUserAsync(
|
|
new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "Admin" },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeTrue();
|
|
store.Verify(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
store.Verify(s => s.AddToRoleAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
email.Verify(
|
|
e => e.SendEmailAsync(
|
|
"n@x.com",
|
|
"Login Information",
|
|
It.Is<string>(body => ContainsStrongGeneratedPassword(body))),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddUser_CreateFails_ReturnsFirstError()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByIdAsync(It.IsAny<string>())).ReturnsAsync((ApplicationUser?)null);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out var store);
|
|
store.As<IUserPasswordStore<ApplicationUser>>()
|
|
.Setup(s => s.SetPasswordHashAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
store.Setup(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "duplicate" }));
|
|
|
|
var outcome = await service.AddUserAsync(
|
|
new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "Admin" },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be("duplicate");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddUser_NonAdmin_CannotSetAccountId_OnSelfOrOthers()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(99)).ReturnsAsync(true);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _, accounts);
|
|
|
|
var selfOutcome = await service.AddUserAsync(
|
|
new AddUserRequestDTO { Name = "Me", Email = "me@x.com", Role = "User", AccountId = 99 },
|
|
Principal("User"),
|
|
CancellationToken.None);
|
|
|
|
var otherOutcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO { Id = "other-1", Name = "Other", Email = "o@x.com", Role = "Dispatcher", AccountId = 99 },
|
|
Principal("Dispatcher"),
|
|
CancellationToken.None);
|
|
|
|
selfOutcome.Success.Should().BeFalse();
|
|
selfOutcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
|
otherOutcome.Success.Should().BeFalse();
|
|
otherOutcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
|
|
|
userData.Verify(u => u.UpdateUserAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
accounts.Verify(a => a.ExistsAsync(It.IsAny<int>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditUser_NonAdmin_CannotChangeOwnAccountScope()
|
|
{
|
|
var existing = IdentityTestHelpers.User("caller-1");
|
|
existing.AccountId = 1;
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("caller-1", It.IsAny<CancellationToken>())).ReturnsAsync(existing);
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(2)).ReturnsAsync(true);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _, accounts);
|
|
|
|
var outcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO
|
|
{
|
|
Id = "caller-1",
|
|
Name = "Alice",
|
|
Email = "alice@example.com",
|
|
Role = "User",
|
|
AccountId = 2
|
|
},
|
|
Principal("User"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
|
existing.AccountId.Should().Be(1);
|
|
userData.Verify(u => u.UpdateUserAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddUser_Admin_InvalidAccountId_RejectsWithoutCreate()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(404)).ReturnsAsync(false);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out var store, accounts);
|
|
|
|
var outcome = await service.AddUserAsync(
|
|
new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "User", AccountId = 404 },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.AccountNotFound);
|
|
store.Verify(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
userData.Verify(u => u.GetByIdAsync(It.IsAny<string>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditUser_Admin_InvalidAccountId_RejectsWithoutSave()
|
|
{
|
|
var existing = IdentityTestHelpers.User("u9");
|
|
existing.AccountId = 1;
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("u9", It.IsAny<CancellationToken>())).ReturnsAsync(existing);
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(404)).ReturnsAsync(false);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out _, accounts);
|
|
|
|
var outcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO { Id = "u9", Name = "N", Email = "n@x.com", Role = "User", AccountId = 404 },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeFalse();
|
|
outcome.Error.Should().Be(UserMutationErrors.AccountNotFound);
|
|
existing.AccountId.Should().Be(1);
|
|
userData.Verify(u => u.UpdateUserAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddUser_Admin_ValidAccountId_PersistsOnCreate()
|
|
{
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetByIdAsync(It.IsAny<string>())).ReturnsAsync((ApplicationUser?)null);
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(10)).ReturnsAsync(true);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out var store, accounts);
|
|
store.As<IUserPasswordStore<ApplicationUser>>()
|
|
.Setup(s => s.SetPasswordHashAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
ApplicationUser? created = null;
|
|
store.Setup(s => s.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()))
|
|
.Callback<ApplicationUser, CancellationToken>((u, _) => created = u)
|
|
.ReturnsAsync(IdentityResult.Success);
|
|
store.Setup(s => s.AddToRoleAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var outcome = await service.AddUserAsync(
|
|
new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "User", AccountId = 10 },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeTrue();
|
|
created.Should().NotBeNull();
|
|
created!.AccountId.Should().Be(10);
|
|
accounts.Verify(a => a.ExistsAsync(10), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditUser_Admin_ValidAccountId_PersistsOnEdit()
|
|
{
|
|
var existing = IdentityTestHelpers.User("u9");
|
|
existing.AccountId = 1;
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("u9", It.IsAny<CancellationToken>())).ReturnsAsync(existing);
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(10)).ReturnsAsync(true);
|
|
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out var store, accounts);
|
|
store.Setup(s => s.GetRolesAsync(existing, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<string> { "User" });
|
|
store.Setup(s => s.RemoveFromRoleAsync(existing, "User", It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
store.Setup(s => s.AddToRoleAsync(existing, "User", It.IsAny<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var outcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO { Id = "u9", Name = "N", Email = "n@x.com", Role = "User", AccountId = 10 },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeTrue();
|
|
existing.AccountId.Should().Be(10);
|
|
userData.Verify(u => u.UpdateUserAsync(existing, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
private static bool ContainsStrongGeneratedPassword(string body)
|
|
{
|
|
const string marker = "Your password is: ";
|
|
var start = body.IndexOf(marker, StringComparison.Ordinal);
|
|
if (start < 0)
|
|
return false;
|
|
|
|
var password = body[(start + marker.Length)..]
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.FirstOrDefault();
|
|
|
|
return password is
|
|
{
|
|
Length: 8
|
|
}
|
|
&& password.Any(char.IsUpper)
|
|
&& password.Any(char.IsLower)
|
|
&& password.Any(char.IsDigit)
|
|
&& password.Any(character => !char.IsLetterOrDigit(character));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2, "alice@example.com", "Dispatcher", true)]
|
|
[InlineData(1, "alice@example.com", "Scheduler", true)]
|
|
[InlineData(1, "alice.new@example.com", "Dispatcher", true)]
|
|
[InlineData(1, "ALICE@example.com", "dispatcher", false)]
|
|
public async Task EditUser_EndsEarlierSessionsOnlyWhenTheTokenClaimsChange(
|
|
int accountId, string email, string role, bool endsSessions)
|
|
{
|
|
var existing = IdentityTestHelpers.User("u1", userName: "alice@example.com");
|
|
existing.AccountId = 1;
|
|
var stampBefore = existing.SecurityStamp;
|
|
var userData = new Mock<IUserDataService>();
|
|
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(existing);
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsAsync(It.IsAny<int>())).ReturnsAsync(true);
|
|
var sessions = new Mock<ISessionStampService>();
|
|
var service = NewService(userData, new Mock<IEmailSender>(), out var store, accounts, sessions);
|
|
store.Setup(s => s.GetRolesAsync(existing, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<string> { "Dispatcher" });
|
|
|
|
var outcome = await service.EditUserAsync(
|
|
new EditUserRequestDTO { Id = "u1", Name = "Alice", Email = email, Role = role, AccountId = accountId },
|
|
Principal("Admin"),
|
|
CancellationToken.None);
|
|
|
|
outcome.Success.Should().BeTrue();
|
|
if (endsSessions)
|
|
{
|
|
existing.SecurityStamp.Should().NotBe(stampBefore);
|
|
sessions.Verify(s => s.Forget("u1"), Times.Once);
|
|
}
|
|
else
|
|
{
|
|
existing.SecurityStamp.Should().Be(stampBefore);
|
|
sessions.Verify(s => s.Forget(It.IsAny<string>()), Times.Never);
|
|
}
|
|
}
|
|
}
|