提交管理员操作一切

This commit is contained in:
chfei committed 2026-10-23 18:16:05 +08:00
1 parent a87ebeaffb
commit d6471f97d3
23 files changed
+600 -73

No files matched your search

@@ -0,0 +1,44 @@
using MAX.Framework.DependencyInjection;
using MAX.Framework.Security;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Store.Users.Tests;
public sealed class DevelopmentTokenTests
{
[Theory]
[InlineData("Development", true, "test-fixed-token", "admin")]
[InlineData("Development", true, "incorrect-token", "")]
[InlineData("Development", false, "test-fixed-token", "")]
[InlineData("Production", true, "test-fixed-token", "")]
public async Task FixedToken_OnlyAuthenticatesWhenEnabledInDevelopment(
string environment, bool enabled, string token, string expectedUser)
{
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = environment });
builder.WebHost.UseTestServer();
builder.Logging.ClearProviders();
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Jwt:DevelopmentToken:Enabled"] = enabled.ToString(),
["Jwt:DevelopmentToken:Token"] = "test-fixed-token",
["Jwt:DevelopmentToken:UserName"] = "admin"
});
builder.Services.AddStoreHubJwt(builder.Configuration);
await using var app = builder.Build();
app.UseAuthentication();
app.MapGet("/identity", (ICurrentUser user) =>
user.IsAuthenticated ? user.UserName : "");
await app.StartAsync();
using var client = app.GetTestClient();
client.DefaultRequestHeaders.Authorization = new("Bearer", token);
Assert.Equal(expectedUser, await client.GetStringAsync("/identity"));
// 同一凭据不依赖首次请求创建的会话。
Assert.Equal(expectedUser, await client.GetStringAsync("/identity"));
}
}
+2
View File
@@ -36,3 +36,5 @@ dotnet test Tests/Store.Users.Tests/Store.Users.Tests.csproj --filter FullyQuali
SQLite 能验证数据持久化和异常回滚,但不能替代 MySQL InnoDB 并发行锁、隔离级别和迁移脚本验证。原有 `UserManagementSmoke` 和 `ShopRenewSmoke` 冒烟入口仍可单独运行。
User DTO 仅声明 Required。RequestValidatorTests 验证非空规则、错误集合和这一模块约定,同时确认公共工具仍支持其他模块的完整 DataAnnotations。UserServiceValidationTests 验证服务层的长度、范围和 ID 判断;原有金额、精度、格式、日期等用例继续通过服务入口验证。HTTP 测试分别验证 DTO 非空拦截与服务手动参数检查,都按系统约定返回 HTTP 200 和 ValidationError。
UserAccessTests 覆盖统一管理范围:管理员跨主账号修改/创建子账号/重置密码,主账号修改本人和直属子账号、重置子账号密码、处理子账号充值备注,非授权账户拒绝操作;普通主账号不能接管管理员账户,也不能充值、扣款或改计费。店铺续费通过真实 MediatR 事务管道测试代操作权限、目标账户扣款与实际操作者审计。HTTP 测试覆盖资料修改与密码重置接口。
+189
View File
@@ -0,0 +1,189 @@
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(target.Id, new() { 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(target.Id, new() { 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(context.Main.Id,
new() { SaLevel = 1 }, 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(privilegedChild.Id,
new() { 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(target.Id,
new() { 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();
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("child", "shop", 1, OperatorName: actor);
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);
}
}
+7 -7
View File
@@ -63,9 +63,9 @@ public sealed class UserFundsTests
using var context = new UserTestContext();
await context.Funds.Change(Request(), true, default);
context.CurrentUser.UserName = "main";
await context.Funds.Change(Request("转移", 30m, "other", "transfer"), true, default);
await context.Funds.Change(Request("转移", 30m, "child", "transfer"), true, default);
var sender = await context.Read("main");
var receiver = await context.Read("other");
var receiver = await context.Read("child");
Assert.Equal(70m, sender.Points);
Assert.Equal(30m, receiver.Points);
Assert.Equal(100m, sender.RechargePointsTotal);
@@ -76,7 +76,7 @@ public sealed class UserFundsTests
var credit = Assert.Single(records, r => r.OptionType == "转入");
Assert.Equal(30m, debit.DeltaPoints);
Assert.Equal(debit.DeltaPoints, credit.DeltaPoints);
Assert.Equal("other", debit.CounterpartyAccountName);
Assert.Equal("child", debit.CounterpartyAccountName);
Assert.Equal("main", credit.CounterpartyAccountName);
Assert.All(records, r => Assert.Equal("main", r.OperatorName));
}
@@ -97,8 +97,8 @@ public sealed class UserFundsTests
public async Task InsufficientFunds_DoesNotChangeAccountsOrCreateRecords(bool points)
{
using var context = new UserTestContext();
if (points) context.CurrentUser.UserName = "other";
var error = await Assert.ThrowsAsync<AppException>(() => context.Funds.Change(Request(points ? "转移" : "消费"), points, default));
if (points) context.CurrentUser.UserName = "main";
var error = await Assert.ThrowsAsync<AppException>(() => context.Funds.Change(Request(points ? "转移" : "消费", target: points ? "child" : "main"), points, default));
Assert.Contains("不足", error.Message);
Assert.Equal(0m, (await context.Read("main")).Points);
Assert.Equal(0m, (await context.Read("other")).Points);
@@ -181,10 +181,10 @@ public sealed class UserFundsTests
? "CREATE TRIGGER fail_audit BEFORE INSERT ON points WHEN NEW.option_type = '转入' BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END"
: "CREATE TRIGGER fail_audit BEFORE INSERT ON payments BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END";
await context.Db.Ado.ExecuteNonQueryAsync(trigger);
var request = Request(points ? "转移" : "充值", 20m, "other", "retry");
var request = Request(points ? "转移" : "充值", 20m, "child", "retry");
var error = await Assert.ThrowsAnyAsync<Exception>(() => context.Funds.Change(request, points, default));
Assert.Contains("forced audit failure", error.Message);
Assert.Equal(0m, points ? (await context.Read("other")).Points : (await context.Read("other")).Balance);
Assert.Equal(0m, points ? (await context.Read("child")).Points : (await context.Read("child")).Balance);
Assert.Equal(0, await context.Db.Select<UserPointsRecordEntity>().Where(r => r.OutTradeNo == "user:retry").CountAsync());
Assert.Equal(0, await context.Db.Select<UserPaymentRecordEntity>().CountAsync());
if (points) Assert.Equal(100m, (await context.Read("main")).Points);
+48
View File
@@ -239,6 +239,54 @@ public sealed class UserHttpTests : IAsyncLifetime
Assert.Equal(0, await _context.Db.Select<Store.Users.Entities.UserTopUpQrcodeEntity>().CountAsync());
}
[Fact]
public async Task GetSubUser_UsesAdminOrMainAccountScope()
{
_context.Seed("foreign-child", parent: "other");
using var allResponse = await _client.PostAsJsonAsync("/v1/user/GetSubUser", new { });
var all = await allResponse.Content.ReadFromJsonAsync<ApiResult<UserPage<UserInfo>>>();
Assert.Equal(HttpStatusCode.OK, allResponse.StatusCode);
Assert.True(all!.Success);
Assert.Equal(2, all.Data!.Total);
using var filteredResponse = await _client.PostAsJsonAsync("/v1/user/GetSubUser", new { parentUserName = "other" });
var filtered = await filteredResponse.Content.ReadFromJsonAsync<ApiResult<UserPage<UserInfo>>>();
Assert.True(filtered!.Success);
Assert.Equal("foreign-child", Assert.Single(filtered.Data!.Items).UserName);
_client.DefaultRequestHeaders.Remove("X-Test-User");
_client.DefaultRequestHeaders.Add("X-Test-User", "main");
using var ownResponse = await _client.PostAsJsonAsync("/v1/user/GetSubUser", new { });
var own = await ownResponse.Content.ReadFromJsonAsync<ApiResult<UserPage<UserInfo>>>();
Assert.True(own!.Success);
Assert.Equal("child", Assert.Single(own.Data!.Items).UserName);
using var forbiddenResponse = await _client.PostAsJsonAsync("/v1/user/GetSubUser", new { parentUserName = "other" });
Assert.Equal(HttpStatusCode.OK, forbiddenResponse.StatusCode);
var forbidden = await forbiddenResponse.Content.ReadFromJsonAsync<ApiResult>();
Assert.Equal(ResultCode.Forbidden, forbidden!.Code);
}
[Fact]
public async Task UpdateEndpoints_ApplySharedAccountManagementScope()
{
using var adminUpdate = await _client.PostAsJsonAsync("/v1/user/UpdateSubUser", new { id = _context.Child.Id, notes = "admin-update" });
Assert.True((await adminUpdate.Content.ReadFromJsonAsync<ApiResult<UserInfo>>())!.Success);
_client.DefaultRequestHeaders.Remove("X-Test-User");
_client.DefaultRequestHeaders.Add("X-Test-User", "main");
foreach (var id in new[] { _context.Main.Id, _context.Child.Id })
{
using var update = await _client.PostAsJsonAsync("/v1/user/UpdateUser", new { id, notes = "main-update" });
Assert.Equal(HttpStatusCode.OK, update.StatusCode);
Assert.True((await update.Content.ReadFromJsonAsync<ApiResult<UserInfo>>())!.Success);
}
using var forbidden = await _client.PostAsJsonAsync("/v1/user/UpdateUser", new { id = _context.Other.Id, notes = "bad" });
Assert.Equal(ResultCode.Forbidden, (await forbidden.Content.ReadFromJsonAsync<ApiResult>())!.Code);
using var reset = await _client.PostAsJsonAsync("/v1/user/ChangePassword", new { userName = "child", newPassword = "reset-password" });
Assert.True((await reset.Content.ReadFromJsonAsync<ApiResult>())!.Success);
Assert.NotNull(await _context.Authentication.Authenticate("child", "reset-password"));
}
[Fact]
public void ApiExplorerAndEndpointMetadata_UsePostForEveryAction()
{
+46 -1
View File
@@ -202,7 +202,7 @@ public sealed class UserManagementTests
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(403, (await Assert.ThrowsAsync<AppException>(() => context.Users.Search(new(), false, default))).Code);
Assert.Equal(3, (await context.Users.Search(new(), false, default)).Total);
}
[Theory]
@@ -215,6 +215,51 @@ public sealed class UserManagementTests
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()
{
+3 -2
View File
@@ -24,14 +24,15 @@ public sealed class UserTopUpTests
}
[Fact]
public async Task ProcessingMessage_RequiresAdmin_AndDoesNotRechargeFunds()
public async Task ProcessingMessage_UsesAccountScope_AndDoesNotRechargeFunds()
{
using var context = new UserTestContext();
context.CurrentUser.UserName = "main";
var id = await context.Users.AddTopUpMessage(new() { Message = "充值待核实" }, default);
context.CurrentUser.UserName = "other";
var error = await Assert.ThrowsAsync<AppException>(() => context.Users.UpdateTopUpMessage(new() { Id = id, Result = "已核实" }, default));
Assert.Equal(403, error.Code);
context.CurrentUser.UserName = "admin";
context.CurrentUser.UserName = "main";
await context.Users.UpdateTopUpMessage(new() { Id = id, Result = "已核实" }, default);
Assert.Equal("已核实", (await context.Users.TopUpMessages(new(), default)).Items.Single().Result);
Assert.Equal(0m, (await context.Read("main")).Points);
+10 -9
View File
@@ -42,8 +42,8 @@ try
Assert(await auth.Authenticate("main", "wrong") == null, "错误密码被接受");
Assert(await auth.Authenticate(" main ", "legacy-password") != null, "旧密码登录失败");
var stored = await db.Select<UserEntity>().Where(u => u.Id == main.Id).FirstAsync();
Assert(stored.Password != "legacy-password" && UserPassword.Verify(stored, "legacy-password"), "旧密码未升级");
Assert(await auth.Authenticate("main", "legacy-password") != null, "哈希密码登录失败");
Assert(stored.Password == "legacy-password" && UserPassword.Verify(stored, "legacy-password"), "登录不应修改已有密码");
Assert(await auth.Authenticate("main", "legacy-password") != null, "重复登录失败");
var created = await users.Create(new() { UserName = "newmain", Password = "new-password" }, false, default);
Assert(created.GroupId == 3 && created.ShopPointsPerMonth == 30 && created.Points == 0, "主账号默认值错误");
Assert(!JsonSerializer.Serialize(created).Contains("Password", StringComparison.OrdinalIgnoreCase), "响应包含密码字段");
@@ -55,7 +55,7 @@ try
await Reject(() => users.Create(new() { UserName = "bad-admin", Password = "password", SaLevel = 1 }, true, default));
await Reject(() => users.Update(sub.Id, new() { ShopPointsPerMonth = 30 }, true, default));
await Reject(() => users.GetUser(other.UserName, default));
await Reject(() => users.Search(new(), false, default));
Assert((await users.Search(new(), false, default)).Total == 2, "主账号列表范围错误");
await Reject(() => users.Update(other.Id, new() { Notes = "tampered" }, true, default));
await users.Update(sub.Id, new() { Notes = "" }, true, default);
Assert((await users.Search(new(), true, default)).Total == 1, "子账号查询范围错误");
@@ -70,10 +70,10 @@ try
await Reject(() => funds.Change(recharge, true, default));
Assert((await db.Select<UserEntity>().Where(u => u.Id == main.Id).FirstAsync()).Points == 100, "重复请求造成重复充值");
actor.UserName = "main";
await funds.Change(new() { UserName = "other", Amount = 30, Operation = "转移", RequestId = "transfer-1" }, true, default);
await funds.Change(new() { UserName = "child", Amount = 30, Operation = "转移", RequestId = "transfer-1" }, true, default);
Assert((await db.Select<UserEntity>().Where(u => u.Id == main.Id).FirstAsync()).Points == 70, "转出错误");
Assert((await db.Select<UserEntity>().Where(u => u.Id == other.Id).FirstAsync()).Points == 30, "转入错误");
await Reject(() => funds.Change(new() { UserName = "other", Amount = 1000, Operation = "转移", RequestId = "overdraft" }, true, default));
Assert((await db.Select<UserEntity>().Where(u => u.Id == sub.Id).FirstAsync()).Points == 30, "转入错误");
await Reject(() => funds.Change(new() { UserName = "child", Amount = 1000, Operation = "转移", RequestId = "overdraft" }, true, default));
Assert(await db.Select<UserPointsRecordEntity>().Where(r => r.OutTradeNo == "user:overdraft").CountAsync() == 0, "失败转账产生流水");
actor.UserName = "admin";
await funds.Change(new() { UserName = "main", Amount = 25.5m, Operation = "充值", RequestId = "balance-1" }, false, default);
@@ -100,14 +100,15 @@ try
actor.UserName = "main";
await db.Ado.ExecuteNonQueryAsync("CREATE TRIGGER reject_credit BEFORE INSERT ON points WHEN NEW.option_type = '转入' BEGIN SELECT RAISE(ABORT, 'test rollback'); END");
bool failed = false;
try { await funds.Change(new() { UserName = "other", Amount = 10, Operation = "转移", RequestId = "rollback" }, true, default); }
try { await funds.Change(new() { UserName = "child", Amount = 10, Operation = "转移", RequestId = "rollback" }, true, default); }
catch (Exception ex) when (ex.Message.Contains("test rollback")) { failed = true; }
Assert(failed, "测试故障未触发");
Assert((await db.Select<UserEntity>().Where(u => u.Id == main.Id).FirstAsync()).Points == 70, "转账失败未回滚转出账户");
Assert((await db.Select<UserEntity>().Where(u => u.Id == other.Id).FirstAsync()).Points == 30, "转账失败未回滚转入账户");
Assert((await db.Select<UserEntity>().Where(u => u.Id == sub.Id).FirstAsync()).Points == 30, "转账失败未回滚转入账户");
Assert(await db.Select<UserPointsRecordEntity>().Where(r => r.OutTradeNo == "user:rollback").CountAsync() == 0, "转账失败未回滚流水");
await db.Ado.ExecuteNonQueryAsync("DROP TRIGGER reject_credit");
var messageId = await users.AddTopUpMessage(new() { Message = "银行充值待核实" }, default);
actor.UserName = "other";
await Reject(() => users.UpdateTopUpMessage(new() { Id = messageId, Result = "已处理" }, default));
actor.UserName = "other";
Assert((await users.TopUpMessages(new(), default)).Total == 0, "充值备注越权泄漏");
@@ -125,7 +126,7 @@ try
var shopOwner = Seed("shop-owner", 0);
await db.Insert(new ShopEntity { UserName = shopOwner.UserName, ShopId = "test-shop" }).ExecuteAffrowsAsync();
await Reject(() => users.Delete(shopOwner.Id, default));
Console.WriteLine("PASS: 用户认证、密码升级、权限隔离、主子账号、计费继承、资金事务、重复请求、超扣回滚、删除限制、参数边界。");
Console.WriteLine("PASS: 用户认证、登录不修改密码、权限隔离、主子账号、计费继承、资金事务、重复请求、超扣回滚、删除限制、参数边界。");
UserEntity Seed(string name, short level)
{