301 lines
13 KiB
C#
301 lines
13 KiB
C#
using Store.Users.Contracts.Enums;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Text.Json;
|
|
using MAX.Framework.Commons;
|
|
using Store.Users.Contracts.Dtos;
|
|
using Store.Users.Entities;
|
|
using YourNamespace;
|
|
|
|
namespace Store.Users.Tests;
|
|
|
|
public sealed class UserManagementTests
|
|
{
|
|
[Theory]
|
|
[InlineData(false, "main")]
|
|
[InlineData(true, null)]
|
|
[InlineData(true, "missing")]
|
|
public async Task MissingIdentity_IsRejected(bool authenticated, string? name)
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.IsAuthenticated = authenticated;
|
|
context.CurrentUser.UserName = name;
|
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => context.Users.GetUser(null, default));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("main", "main")]
|
|
[InlineData("main", "child")]
|
|
[InlineData("admin", "other")]
|
|
[InlineData("child", "child")]
|
|
public async Task AuthorizedAccount_CanBeRead(string actor, string target)
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = actor;
|
|
var info = await context.Users.GetUser(target, default);
|
|
Assert.Equal(target, info.UserName);
|
|
Assert.Null(info.Password);
|
|
Assert.DoesNotContain("password", JsonSerializer.Serialize(info), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("main", "other")]
|
|
[InlineData("child", "main")]
|
|
[InlineData("other", "child")]
|
|
public async Task UnrelatedAccount_IsForbidden(string actor, string target)
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = actor;
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.GetUser(target, default));
|
|
Assert.Equal(403, error.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateMainAccount_InitializesDefaultsAndHashesPassword()
|
|
{
|
|
using var context = new UserTestContext();
|
|
var info = await context.Users.Create(new() { UserName = "created", Password = "new-password" }, false, default);
|
|
var stored = await context.Read("created");
|
|
Assert.True(info.IsMainUser);
|
|
Assert.Equal(UserGroup.MainAccount, info.GroupId);
|
|
Assert.Equal(30, info.ShopPointsPerMonth);
|
|
Assert.Equal(0, info.Points);
|
|
Assert.Equal(0m, info.Balance);
|
|
Assert.NotEqual("new-password", stored.Password);
|
|
Assert.NotNull(stored.AddTime);
|
|
Assert.Null(info.Password);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DuplicateUsername_IsRejectedWithoutCreatingAnAccount()
|
|
{
|
|
using var context = new UserTestContext();
|
|
var count = await context.Db.Select<UserEntity>().CountAsync();
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Create(
|
|
new() { UserName = " main ", Password = "new-password" }, false, default));
|
|
Assert.Equal("用户名已存在", error.Message);
|
|
Assert.Equal(count, await context.Db.Select<UserEntity>().CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NonAdmin_CannotCreateMainAccount()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "main";
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Create(
|
|
new() { UserName = "created", Password = "new-password" }, false, default));
|
|
Assert.Equal(403, error.Code);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(4)]
|
|
[InlineData(5)]
|
|
[InlineData(8)]
|
|
[InlineData(9)]
|
|
[InlineData(10)]
|
|
[InlineData(11)]
|
|
public async Task MainAccount_CreatesChildWithServerAssignedParent(byte groupId)
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "main";
|
|
var info = await context.Users.Create(new()
|
|
{
|
|
UserName = "created", Password = "new-password", GroupId = (UserGroup)groupId, Authority = "订单"
|
|
}, true, default);
|
|
Assert.Equal("main", info.ParentUserName);
|
|
Assert.Equal((UserGroup)groupId, info.GroupId);
|
|
Assert.Equal(UserAdminLevel.Normal, info.SaLevel);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(255)]
|
|
public async Task ChildGroup_MustBeOneOfSupportedGroups(byte groupId)
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "main";
|
|
await Assert.ThrowsAsync<ValidationException>(() => context.Users.Create(new()
|
|
{
|
|
UserName = "created", Password = "new-password", GroupId = (UserGroup)groupId
|
|
}, true, default));
|
|
Assert.False(await context.Db.Select<UserEntity>().Where(u => u.UserName == "created").AnyAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MainGroup_CannotBeUsedForChildAccount()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "main";
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Create(new()
|
|
{
|
|
UserName = "created", Password = "new-password", GroupId = UserGroup.MainAccount
|
|
}, true, default));
|
|
Assert.Contains("子账号不能使用主号分组", error.Message);
|
|
}
|
|
[Fact]
|
|
public async Task ChildAccount_CannotCreateAnotherChild()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "child";
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Create(
|
|
new() { UserName = "created", Password = "new-password" }, true, default));
|
|
Assert.Equal(403, error.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MainAccount_CannotGrantAdditionalAuthorityOrAdministratorLevel()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "main";
|
|
foreach (var request in new UserUpdateRequest[]
|
|
{
|
|
new() { Authority = "不存在的权限" }, new() { SaLevel = UserAdminLevel.SuperAdmin }, new() { ShopPointsPerMonth = 30 }
|
|
})
|
|
{
|
|
request.Id = context.Child.Id;
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Update(request, true, default));
|
|
Assert.Equal(403, error.Code);
|
|
}
|
|
Assert.Equal(UserAdminLevel.Normal, (await context.Read("child")).SaLevel);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_PreservesNullFields_ClearsEmptyFields_AndKeepsFunds()
|
|
{
|
|
using var context = new UserTestContext();
|
|
await context.Db.Update<UserEntity>().Set(u => u.Notes, "old note").Set(u => u.Mobile, "13800000000")
|
|
.Set(u => u.Points, 42m).Set(u => u.Balance, 11m).Where(u => u.Id == context.Main.Id).ExecuteAffrowsAsync();
|
|
var info = await context.Users.Update(new() { Id = context.Main.Id, Notes = "", NickName = " New Name " }, false, default);
|
|
Assert.Equal("", info.Notes);
|
|
Assert.Equal("13800000000", info.Mobile);
|
|
Assert.Equal("New Name", info.NickName);
|
|
Assert.Equal(42m, info.Points);
|
|
Assert.Equal(11m, info.Balance);
|
|
Assert.Contains("main", context.Sessions.SignedOut);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Child_InheritsOemFeesAndPointsAuthorityFromParent()
|
|
{
|
|
using var context = new UserTestContext();
|
|
await context.Db.Update<UserEntity>().Set(u => u.OemId, "brand").Set(u => u.PostagePerOrder, 1m)
|
|
.Set(u => u.RefundPostagePerOrder, 1m).Set(u => u.GiftOemAmountPerOrder, -1m)
|
|
.Set(u => u.SyncLogisticsPerOrder, 0.5m).Where(u => u.Id == context.Main.Id).ExecuteAffrowsAsync();
|
|
var info = await context.Query.GetUser("child");
|
|
Assert.Equal("brand", info.OemId);
|
|
Assert.Equal(1m, info.PostagePerOrder);
|
|
Assert.Equal(1m, info.RefundPostagePerOrder);
|
|
Assert.Equal(-1m, info.GiftOemAmountPerOrder);
|
|
Assert.Equal(0.5m, info.SyncLogisticsPerOrder);
|
|
await context.Db.Update<UserEntity>().Set(u => u.Authority, "订单").Where(u => u.Id == context.Main.Id).ExecuteAffrowsAsync();
|
|
Assert.DoesNotContain("积分用户", (await context.Query.GetUser("child")).Authority);
|
|
// 有效查询结果不能把继承值写回子账号原始配置。
|
|
Assert.Equal("", (await context.Read("child")).OemId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Search_FiltersAndPaginatesWithoutLeakingOtherChildren()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.Seed("child-two", parent: "main");
|
|
context.Seed("foreign-child", parent: "other");
|
|
context.CurrentUser.UserName = "main";
|
|
var page = await context.Users.Search(new() { Keyword = "child", GroupId = UserGroup.CustomerService, PageSize = 1 }, true, default);
|
|
Assert.Equal(2, page.Total);
|
|
Assert.Equal("child-two", Assert.Single(page.Items).UserName);
|
|
var second = await context.Users.Search(new() { Page = 2, PageSize = 1 }, true, default);
|
|
Assert.Equal("child", Assert.Single(second.Items).UserName);
|
|
Assert.Equal(3, (await context.Users.Search(new(), false, default)).Total);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, 50)]
|
|
[InlineData(1, 0)]
|
|
[InlineData(1, 201)]
|
|
public async Task InvalidPagination_IsRejected(int page, int pageSize)
|
|
{
|
|
using var context = new UserTestContext();
|
|
await Assert.ThrowsAsync<ValidationException>(() => context.Users.Search(new() { Page = page, PageSize = pageSize }, false, default));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Admin_CanQueryAllChildrenWithoutIncludingMainAccounts()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.Seed("foreign-child", parent: "other");
|
|
context.Seed("admin-child", parent: "admin");
|
|
context.Seed("blank-parent", parent: " ");
|
|
var page = await context.Users.Search(new(), true, default);
|
|
Assert.Equal(3, page.Total);
|
|
Assert.Equal(new[] { "admin-child", "child", "foreign-child" }, page.Items.Select(u => u.UserName).Order().ToArray());
|
|
Assert.All(page.Items, u => Assert.False(u.IsMainUser));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("main", "child")]
|
|
[InlineData(" other ", "foreign-child")]
|
|
public async Task Admin_CanFilterChildrenByAnyParent(string parent, string expectedChild)
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.Seed("foreign-child", parent: "other");
|
|
var page = await context.Users.Search(new() { ParentUserName = parent }, true, default);
|
|
Assert.Equal(1, page.Total);
|
|
Assert.Equal(expectedChild, Assert.Single(page.Items).UserName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MainAccount_CannotSelectAnotherParentsChildren()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "main";
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Search(new() { ParentUserName = "other" }, true, default));
|
|
Assert.Equal(403, error.Code);
|
|
var page = await context.Users.Search(new() { ParentUserName = "main" }, true, default);
|
|
Assert.Equal("child", Assert.Single(page.Items).UserName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ChildAccount_CannotQueryChildLists()
|
|
{
|
|
using var context = new UserTestContext();
|
|
context.CurrentUser.UserName = "child";
|
|
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Search(new(), true, default));
|
|
Assert.Equal(403, error.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyAccount_CanBeDeleted_AndSessionIsInvalidated()
|
|
{
|
|
using var context = new UserTestContext();
|
|
await context.Users.Delete(context.Other.Id, default);
|
|
Assert.Null(await context.Read("other"));
|
|
Assert.Contains("other", context.Sessions.SignedOut);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyOwnAccount_CanBeDeleted_AndSessionIsInvalidated()
|
|
{
|
|
using var context = new UserTestContext();
|
|
await context.Users.Delete(context.Admin.Id, default);
|
|
Assert.Null(await context.Read("admin"));
|
|
Assert.Contains("admin", context.Sessions.SignedOut);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("points")]
|
|
[InlineData("balance")]
|
|
[InlineData("children")]
|
|
[InlineData("shops")]
|
|
public async Task AccountWithBlockingCondition_CannotBeDeleted(string condition)
|
|
{
|
|
using var context = new UserTestContext();
|
|
var target = context.Other;
|
|
if (condition == "children") target = context.Main;
|
|
if (condition == "points") await context.Db.Update<UserEntity>().Set(u => u.Points, 1m).Where(u => u.Id == target.Id).ExecuteAffrowsAsync();
|
|
if (condition == "balance") await context.Db.Update<UserEntity>().Set(u => u.Balance, 1m).Where(u => u.Id == target.Id).ExecuteAffrowsAsync();
|
|
if (condition == "shops") await context.Db.Insert(new ShopEntity { ShopId = "shop", UserName = target.UserName }).ExecuteAffrowsAsync();
|
|
await Assert.ThrowsAsync<AppException>(() => context.Users.Delete(target.Id, default));
|
|
Assert.NotNull(await context.Read(target.UserName));
|
|
Assert.Empty(context.Sessions.SignedOut);
|
|
}
|
|
}
|