Files
2026-10-29 11:34:35 +08:00

190 lines
9.1 KiB
C#

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;
/// <summary>管理员、主账号及子账号的管理权限矩阵;资金特权单独验证。</summary>
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<AppException>(() => 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<AppException>(() => context.Users.Create(new()
{
UserName = "new-child", Password = "password", ParentUserName = "other"
}, true, default));
Assert.Equal(403, error.Code);
Assert.Equal(403, (await Assert.ThrowsAsync<AppException>(() => 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<AppException>(() => 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<AppException>(() => 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<AppException>(() => context.Users.ChangePassword(new()
{
UserName = privilegedChild.UserName, NewPassword = "takeover-password"
}, default))).Code);
Assert.Equal(403, (await Assert.ThrowsAsync<AppException>(() => 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<AppException>(() => 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<AppException>(() => context.Users.Update(new() { Id = target.Id, ShopPointsPerMonth = 45 }, false, default))).Code);
}
Assert.Equal(403, (await Assert.ThrowsAsync<AppException>(() => 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<ShopExpireEntity>(e => e.Name("shop_expirations"));
context.Db.CodeFirst.SyncStructure<ShopExpireEntity>();
await context.Db.Update<UserEntity>().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<MAX.Framework.Security.ICurrentUser>(context.CurrentUser);
services.AddSingleton(context.Db);
services.AddSingleton<UnitOfWorkExecutionContext>();
services.AddMediatRAttributedBehaviors(typeof(ShopRenewCommand).Assembly);
services.AddTransient(typeof(IRequestHandler<ShopRenewCommand, ShopRenewResponse>),
typeof(ShopRenewCommand).Assembly.GetType("Store.Users.Commands.ShopRenewCommandHandler", true)!);
using var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
var command = new ShopRenewCommand(new() { UserName = "child", ShopId = "shop", RenewDuration = 1 });
if (!allowed)
{
Assert.Equal(403, (await Assert.ThrowsAsync<AppException>(() => mediator.Send(command))).Code);
Assert.Equal(100m, (await context.Read("child")).Points);
Assert.Equal(0, await context.Db.Select<UserPointsRecordEntity>().CountAsync());
return;
}
await mediator.Send(command);
Assert.Equal(70m, (await context.Read("child")).Points);
var record = Assert.Single(await context.Db.Select<UserPointsRecordEntity>().ToListAsync());
Assert.Equal(actor, record.OperatorName);
Assert.Equal("child", record.UserName);
Assert.Equal(actor == "admin" ? "管理员" : "用户", record.OperatorType);
}
}