using System.ComponentModel.DataAnnotations; using System.Reflection; using System.Security.Claims; using System.Text.Json; using FreeSql; using MAX.Framework.Commons; using MAX.Framework.Security; using Store.Users.Contracts; using Store.Users.Contracts.Dtos; using Store.Users.Entities; using Store.Users.Services; using YourNamespace; UserRoutingChecks.Run(); // 独立临时 SQLite 数据库,绝不连接开发或生产数据库。 var path = Path.Combine(Path.GetTempPath(), $"store-users-{Guid.NewGuid():N}.db"); try { using var db = new FreeSqlBuilder().UseMappingPriority(FreeSql.Internal.MappingPriorityType.Attribute, FreeSql.Internal.MappingPriorityType.Aop, FreeSql.Internal.MappingPriorityType.FluentApi).UseConnectionString(FreeSql.DataType.Sqlite, $"Data Source={path}").Build(); db.CodeFirst.ConfigEntity(e => e.Name("users")); db.CodeFirst.ConfigEntity(e => e.Name("points")); db.CodeFirst.ConfigEntity(e => e.Name("payments")); db.CodeFirst.ConfigEntity(e => e.Name("shops")); db.CodeFirst.ConfigEntity(e => e.Name("oems")); db.CodeFirst.ConfigEntity(e => e.Name("messages")); db.CodeFirst.ConfigEntity(e => e.Name("qrcodes")); db.CodeFirst.SyncStructure(typeof(UserEntity), typeof(UserPointsRecordEntity), typeof(UserPaymentRecordEntity), typeof(ShopEntity), typeof(OEMEntity), typeof(UserTopUpMessageEntity), typeof(UserTopUpQrcodeEntity)); var admin = Seed("admin", 1); var main = Seed("main", 0); var other = Seed("other", 0); var actor = new Actor { UserName = admin.UserName }; var sessions = new Sessions(); var repositoryType = typeof(UserManagementService).Assembly.GetType("Store.Users.Repositories.UserRepository")!; var repository = Activator.CreateInstance(repositoryType, db)!; var queryType = typeof(UserManagementService).Assembly.GetType("Store.Users.Services.UserQueryService")!; var query = (IUserQueryService)Activator.CreateInstance(queryType, repository)!; var authType = typeof(UserManagementService).Assembly.GetType("Store.Users.Services.UserAuthenticationService")!; var auth = (IUserAuthenticationService)Activator.CreateInstance(authType, db, query)!; var users = new UserManagementService(db, actor, query, sessions); var funds = new UserFundsService(db, actor); Assert(await auth.Authenticate("main", "wrong") == null, "错误密码被接受"); Assert(await auth.Authenticate(" main ", "legacy-password") != null, "旧密码登录失败"); var stored = await db.Select().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, "重复登录失败"); 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), "响应包含密码字段"); actor.UserName = main.UserName; var sub = await users.Create(new() { UserName = "child", Password = "child-password", Authority = "积分用户" }, true, default); Assert(sub.ParentUserName == main.UserName && sub.OemId == main.OemId, "子账号归属或OEM继承错误"); Assert(sub.PostagePerOrder == 2.2m && sub.RefundPostagePerOrder == 2.5m, "继承费用下限错误"); await Reject(() => users.Create(new() { UserName = "bad", Password = "password", Authority = "超级权限" }, true, default)); 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)); 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, "子账号查询范围错误"); await users.ChangePassword(new() { OldPassword = "legacy-password", NewPassword = "changed-password" }, default); Assert(sessions.Names.Contains("main"), "修改密码未注销会话"); Assert(await auth.Authenticate("main", "legacy-password") == null, "旧密码仍能登录"); Assert(await auth.Authenticate("main", "changed-password") != null, "新密码登录失败"); await Reject(() => funds.Change(new() { UserName = "other", Amount = 1, Operation = "充值", RequestId = "forbidden" }, true, default)); actor.UserName = "admin"; var recharge = new UserFundsRequest { UserName = "main", Amount = 100, Operation = "充值", RequestId = "points-1" }; await funds.Change(recharge, true, default); await Reject(() => funds.Change(recharge, true, default)); Assert((await db.Select().Where(u => u.Id == main.Id).FirstAsync()).Points == 100, "重复请求造成重复充值"); actor.UserName = "main"; await funds.Change(new() { UserName = "child", Amount = 30, Operation = "转移", RequestId = "transfer-1" }, true, default); Assert((await db.Select().Where(u => u.Id == main.Id).FirstAsync()).Points == 70, "转出错误"); Assert((await db.Select().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().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); await funds.Change(new() { UserName = "main", Amount = 5m, Operation = "消费", RequestId = "balance-2" }, false, default); Assert((await db.Select().Where(u => u.Id == main.Id).FirstAsync()).Balance == 20.5m, "余额变更错误"); await Reject(() => users.Delete(main.Id, default)); await Reject(() => users.Delete(admin.Id, default)); await users.Delete(created.Id, default); Assert(!await db.Select().Where(u => u.Id == created.Id).AnyAsync(), "空账户删除失败"); await Reject(() => users.Search(new() { PageSize = 201 }, false, default)); await Reject(() => funds.Change(new() { UserName = "main", Amount = 0.001m, Operation = "充值", RequestId = "precision" }, false, default)); Assert((await users.Points("main", new() { Operation = "充值" }, default)).Total == 1, "积分流水过滤错误"); Assert((await users.Balance("main", new() { PaymentType = 1 }, default)).Total == 1, "余额流水过滤错误"); await Reject(() => users.Points("main", new() { StartTime = DateTime.Today, EndTime = DateTime.Today.AddDays(-1) }, default)); using (var cancelled = new CancellationTokenSource()) { cancelled.Cancel(); bool cancellationObserved = false; try { await auth.Authenticate("main", "changed-password", cancelled.Token); } catch (OperationCanceledException) { cancellationObserved = true; } Assert(cancellationObserved, "认证未响应取消请求"); } // 强制第二条转账流水写入失败,验证已更新的双方积分和第一条流水一起回滚。 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 = "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().Where(u => u.Id == main.Id).FirstAsync()).Points == 70, "转账失败未回滚转出账户"); Assert((await db.Select().Where(u => u.Id == sub.Id).FirstAsync()).Points == 30, "转账失败未回滚转入账户"); Assert(await db.Select().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, "充值备注越权泄漏"); actor.UserName = "admin"; await users.UpdateTopUpMessage(new() { Id = messageId, Result = "已处理" }, default); Assert((await users.TopUpMessages(new(), default)).Items.Single().Result == "已处理", "备注处理结果未保存"); await users.UpdateTopUpQrcode(new() { AgentCode = "agent", QrcodeBase64 = "aGVsbG8=" }, default); await users.UpdateTopUpQrcode(new() { AgentCode = "agent", QrcodeBase64 = "d29ybGQ=" }, default); Assert(await users.GetTopUpQrcode("agent", default) == "d29ybGQ=", "二维码更新失败"); Assert(await db.Select().CountAsync() == 1, "二维码产生重复编码"); await Reject(() => users.UpdateTopUpQrcode(new() { AgentCode = "agent", QrcodeBase64 = "invalid!" }, default)); actor.UserName = "child"; await Reject(() => users.Create(new() { UserName = "grandchild", Password = "password" }, true, default)); actor.UserName = "admin"; 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: 用户认证、登录不修改密码、权限隔离、主子账号、计费继承、资金事务、重复请求、超扣回滚、删除限制、参数边界。"); UserEntity Seed(string name, short level) { var u = new UserEntity { UserName = name, NickName = name, Password = "legacy-password", ParentUserName = "", GroupId = 3, Mobile = "", CompanyName = "", Notes = "", CardId = "", Authority = "积分用户", ForbiddenPage = "", OemId = "", SaLevel = level, Balance = 0, Points = 0, ShopPointsPerMonth = 30, PostagePerOrder = 1, RefundPostagePerOrder = 1 }; u.Id = (int)db.Insert(u).ExecuteIdentity(); return u; } } finally { if (File.Exists(path)) File.Delete(path); } static void Assert(bool condition, string message) { if (!condition) throw new Exception(message); } static async Task Reject(Func action) { try { await action(); } catch (AppException) { return; } catch (ValidationException) { return; } throw new Exception("操作应被拒绝"); } sealed class Actor : ICurrentUser { public bool IsAuthenticated => true; public string? UserName { get; set; } public string? NickName => UserName; public IEnumerable Claims => []; public string? GetClaimValue(string type) => null; public T? GetClaimValue(string type) => default; } sealed class Sessions : IOnlineUserStore { public HashSet Names { get; } = []; public void SignOutSession(string name, string jti) => Names.Add(name); public void SignOut(string name) => Names.Add(name); public void SignIn(OnlineUserInfo info) { } public bool IsTokenActive(string name, string jti) => false; public IReadOnlyList GetOnlineUsers() => []; public int Count => 0; }