Files
StoreWeb/Tests/Store.Users.Tests/UserManagementTests.cs
T
2026-10-23 18:16:05 +08:00

292 lines
12 KiB
C#

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((byte)3, 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 = groupId, Authority = "订单"
}, true, default);
Assert.Equal("main", info.ParentUserName);
Assert.Equal(groupId, info.GroupId);
Assert.Equal((short)0, 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 = 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 = 3
}, 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 UserEditRequest[]
{
new() { Authority = "不存在的权限" }, new() { SaLevel = 1 }, new() { ShopPointsPerMonth = 30 }
})
{
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.Update(context.Child.Id, request, true, default));
Assert.Equal(403, error.Code);
}
Assert.Equal((short)0, (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(context.Main.Id, new() { 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(2.2m, info.PostagePerOrder);
Assert.Equal(2.5m, info.RefundPostagePerOrder);
Assert.Equal(0m, 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 = 4, 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);
}
[Theory]
[InlineData("self")]
[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 == "self") target = context.Admin;
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);
}
}