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(() => 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(() => 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().CountAsync(); var error = await Assert.ThrowsAsync(() => context.Users.Create( new() { UserName = " main ", Password = "new-password" }, false, default)); Assert.Equal("用户名已存在", error.Message); Assert.Equal(count, await context.Db.Select().CountAsync()); } [Fact] public async Task NonAdmin_CannotCreateMainAccount() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; var error = await Assert.ThrowsAsync(() => 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(() => context.Users.Create(new() { UserName = "created", Password = "new-password", GroupId = (UserGroup)groupId }, true, default)); Assert.False(await context.Db.Select().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(() => 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(() => 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(() => 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().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().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().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(() => 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(() => 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(() => 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().Set(u => u.Points, 1m).Where(u => u.Id == target.Id).ExecuteAffrowsAsync(); if (condition == "balance") await context.Db.Update().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(() => context.Users.Delete(target.Id, default)); Assert.NotNull(await context.Read(target.UserName)); Assert.Empty(context.Sessions.SignedOut); } }