using Store.Users.Contracts.Enums; using MAX.Framework.Commons; using MAX.Framework.DependencyInjection; using MAX.Framework.Mediation; using MediatR; using Microsoft.Extensions.DependencyInjection; using Store.Users.Commands; using Store.Users.Contracts.Dtos; using Store.Users.Entities; using YourNamespace; namespace Store.Users.Tests; /// 管理员、主账号及子账号的管理权限矩阵;资金特权单独验证。 public sealed class UserAccessTests { [Theory] [InlineData("admin", "other", false)] [InlineData("admin", "child", true)] [InlineData("admin", "child", false)] [InlineData("main", "main", false)] [InlineData("main", "child", false)] [InlineData("main", "child", true)] public async Task AuthorizedActor_CanUpdateAccount(string actor, string targetName, bool subRoute) { using var context = new UserTestContext(); context.CurrentUser.UserName = actor; var target = await context.Read(targetName); await context.Users.Update(new() { Id = target.Id, Notes = "updated" }, subRoute, default); Assert.Equal("updated", (await context.Read(targetName)).Notes); Assert.Contains(targetName, context.Sessions.SignedOut); } [Theory] [InlineData("main", "other")] [InlineData("child", "main")] [InlineData("child", "child")] public async Task UnauthorizedActor_CannotUpdateAccount(string actor, string targetName) { using var context = new UserTestContext(); context.CurrentUser.UserName = actor; var target = await context.Read(targetName); var error = await Assert.ThrowsAsync(() => context.Users.Update(new() { Id = target.Id, Notes = "bad" }, false, default)); Assert.Equal(403, error.Code); Assert.Equal("", (await context.Read(targetName)).Notes); } [Fact] public async Task Admin_CanCreateChildForAnotherMainAccount() { using var context = new UserTestContext(); var created = await context.Users.Create(new() { UserName = "new-child", Password = "password", ParentUserName = "other" }, true, default); Assert.Equal("other", created.ParentUserName); } [Fact] public async Task Main_CannotCreateChildForAnotherAccount_OrElevateOwnRole() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; var error = await Assert.ThrowsAsync(() => context.Users.Create(new() { UserName = "new-child", Password = "password", ParentUserName = "other" }, true, default)); Assert.Equal(403, error.Code); Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Users.Update(new() { Id = context.Main.Id, SaLevel = UserAdminLevel.SuperAdmin }, false, default))).Code); } [Theory] [InlineData("admin", "other")] [InlineData("admin", "child")] [InlineData("main", "child")] public async Task AuthorizedActor_CanResetManagedAccountPassword(string actor, string target) { using var context = new UserTestContext(); context.CurrentUser.UserName = actor; await context.Users.ChangePassword(new() { UserName = target, NewPassword = "reset-password" }, default); Assert.NotNull(await context.Authentication.Authenticate(target, "reset-password")); Assert.Null(await context.Authentication.Authenticate(target, "legacy-password")); Assert.Contains(target, context.Sessions.SignedOut); Assert.DoesNotContain(actor, context.Sessions.SignedOut); } [Theory] [InlineData("main", "other")] [InlineData("child", "main")] public async Task UnauthorizedActor_CannotResetPassword(string actor, string target) { using var context = new UserTestContext(); context.CurrentUser.UserName = actor; var error = await Assert.ThrowsAsync(() => context.Users.ChangePassword(new() { UserName = target, NewPassword = "reset-password" }, default)); Assert.Equal(403, error.Code); Assert.True(Store.Users.Services.UserPassword.Verify(await context.Read(target), "legacy-password")); } [Fact] public async Task Main_CanCreateReadAndProcessOwnChildsTopUpMessage() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; var id = await context.Users.AddTopUpMessage(new() { UserName = "child", Message = "pending" }, default); await context.Users.UpdateTopUpMessage(new() { Id = id, Result = "done" }, default); var message = Assert.Single((await context.Users.TopUpMessages(new(), default)).Items); Assert.Equal("child", message.UserName); Assert.Equal("done", message.Result); Assert.Equal(0m, (await context.Read("child")).Points); context.CurrentUser.UserName = "other"; Assert.Empty((await context.Users.TopUpMessages(new(), default)).Items); Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Users.UpdateTopUpMessage(new() { Id = id, Result = "bad" }, default))).Code); } [Fact] public async Task Main_CannotTakeOverAnAdministratorAccountThroughChildRelationship() { using var context = new UserTestContext(); var privilegedChild = context.Seed("privileged-child", 1, "main"); context.CurrentUser.UserName = "main"; Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Users.ChangePassword(new() { UserName = privilegedChild.UserName, NewPassword = "takeover-password" }, default))).Code); Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Users.Update(new() { Id = privilegedChild.Id, Notes = "takeover" }, true, default))).Code); Assert.True(Store.Users.Services.UserPassword.Verify(await context.Read(privilegedChild.UserName), "legacy-password")); } [Fact] public async Task Main_CannotRechargeOwnOrChildAccounts_OrModifyBillingConfiguration() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; foreach (var name in new[] { "main", "child" }) { foreach (bool points in new[] { true, false }) Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Funds.Change(new() { UserName = name, Amount = 10m, Operation = "充值", RequestId = Guid.NewGuid().ToString("N") }, points, default))).Code); var target = await context.Read(name); Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Users.Update(new() { Id = target.Id, ShopPointsPerMonth = 45 }, false, default))).Code); } Assert.Equal(403, (await Assert.ThrowsAsync(() => context.Funds.Change(new() { UserName = "other", Amount = 1m, Operation = "转移", RequestId = "outside" }, true, default))).Code); } [Theory] [InlineData("admin", true)] [InlineData("main", true)] [InlineData("other", false)] public async Task ShopRenew_UsesManagedAccountAndAuditsActualActor(string actor, bool allowed) { using var context = new UserTestContext(); context.Db.CodeFirst.ConfigEntity(e => e.Name("shop_expirations")); context.Db.CodeFirst.SyncStructure(); await context.Db.Update().Set(u => u.Points, 100m).Where(u => u.Id == context.Child.Id).ExecuteAffrowsAsync(); await context.Db.Insert(new ShopEntity { ShopId = "shop", ShopAccount = "shop-account", UserName = "child" }).ExecuteAffrowsAsync(); var services = new ServiceCollection(); context.CurrentUser.UserName = actor; services.AddSingleton(context.CurrentUser); services.AddSingleton(context.Db); services.AddSingleton(); services.AddMediatRAttributedBehaviors(typeof(ShopRenewCommand).Assembly); services.AddTransient(typeof(IRequestHandler), typeof(ShopRenewCommand).Assembly.GetType("Store.Users.Commands.ShopRenewCommandHandler", true)!); using var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); var command = new ShopRenewCommand(new() { UserName = "child", ShopId = "shop", RenewDuration = 1 }); if (!allowed) { Assert.Equal(403, (await Assert.ThrowsAsync(() => mediator.Send(command))).Code); Assert.Equal(100m, (await context.Read("child")).Points); Assert.Equal(0, await context.Db.Select().CountAsync()); return; } await mediator.Send(command); Assert.Equal(70m, (await context.Read("child")).Points); var record = Assert.Single(await context.Db.Select().ToListAsync()); Assert.Equal(actor, record.OperatorName); Assert.Equal("child", record.UserName); Assert.Equal(actor == "admin" ? "管理员" : "用户", record.OperatorType); } }