提交管理员操作一切
This commit is contained in:
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"));
|
||||
}
|
||||
}
|
||||
@@ -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 测试覆盖资料修改与密码重置接口。
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user