using Store.Management.Framework.ViewModels; using Store.Management.Models; using Autofac; using Store.Management.Framework.Services; using System.Windows.Controls; using System.Windows; using System.Windows.Threading; using Store.Management; using Store.Management.Views; using Store.Management.ViewModels; using Store.Management.Framework.Controls; using Store.Management.Api; using Store.Management.Api.Users; using System.Net; using System.Net.Http; using System.Text; using Prism.Ioc; using Prism.Services.Dialogs; using DialogWindow = Store.Management.Framework.Controls.DialogWindow; class Program { [STAThread] static int Main(string[] args) { var handler = new LoginHandler(); using var httpClient = new HttpClient(handler); using var client = new StoreApiClient(new ApiClientOptions { BaseUrl = new Uri("http://localhost:14312/api/") }, httpClient); if (args.Contains("--login-service")) { var extension = new Prism.Autofac.AutofacContainerExtension(); extension.FinalizeExtension(); using (var productionContainer = extension.Instance) { var productionClient = productionContainer.Resolve(); Check(ReferenceEquals(productionClient, productionContainer.Resolve()), "自动注册 API 单例"); Check(productionContainer.Resolve() != null, "自动注册登录服务"); } var testExtension = new Prism.Autofac.AutofacContainerExtension(); testExtension.RegisterInstance(typeof(StoreApiClient), client); testExtension.FinalizeExtension(); using var container = testExtension.Instance; Check(ReferenceEquals(client, container.Resolve()), "保留测试客户端注册"); var service = container.Resolve(); using var vm = new LoginViewModel(service); vm.UserName = " admin "; vm.UpdatePassword("wrong"); vm.LoginCommand.Execute().GetAwaiter().GetResult(); Check(!client.Session.IsAuthenticated && vm.CanEdit, "登录失败后可以重试"); ButtonResult? result = null; vm.RequestClose += response => result = response.Result; vm.UpdatePassword(" secret "); vm.LoginCommand.Execute().GetAwaiter().GetResult(); Check(client.Session.IsAuthenticated && result == ButtonResult.OK, "Service 登录成功并通知关闭"); using var closingVm = new LoginViewModel(service); closingVm.UserName = "admin"; closingVm.UpdatePassword(" secret "); Task pending = closingVm.LoginCommand.Execute(); closingVm.Dispose(); pending.GetAwaiter().GetResult(); Check(!closingVm.CanEdit && handler.RequestCount == 3, "关闭取消请求"); Console.WriteLine("PASS: LoginViewModel -> ILoginService -> AuthApi, retry, password contract and cancellation."); return 0; } App app = args.Contains("--status-bar") || args.Contains("--view-lifecycle") ? new App() : new TestApp(client); app.InitializeComponent(); if (args.Contains("--view-lifecycle")) { app.ShutdownMode = ShutdownMode.OnExplicitShutdown; var pageVm = new LifecycleProbe(); var windowVm = new LifecycleProbe(); var page = new UserControl { DataContext = pageVm, Content = new UserControl() }; var window = new Window { DataContext = windowVm, Content = page }; window.Show(); app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle); Check(pageVm.Loads == 1 && windowVm.Loads == 1, $"窗口和页面各加载一次:{windowVm.Loads}/{pageVm.Loads}"); window.Content = null; app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle); Check(pageVm.Unloads == 1, "移出页面触发卸载"); window.Content = page; app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle); Check(pageVm.Loads == 2 && pageVm.FirstLoads == 1, "重新加入页面再次加载"); var replacement = new LifecycleProbe(); page.DataContext = replacement; Check(pageVm.Unloads == 2 && replacement.Loads == 1 && replacement.FirstLoads == 1, "已加载的页面更换 ViewModel 后转移通知"); windowVm.CancelClose = true; window.Close(); Check(window.IsVisible && windowVm.Closes == 0, "ViewModel 可以取消关闭"); windowVm.CancelClose = false; window.Close(); Check(windowVm.Closes == 1, "窗口关闭后通知 ViewModel"); var dialogVm = new LifecycleProbe(); var dialog = new DialogWindow { DataContext = dialogVm, Content = new UserControl { DataContext = dialogVm } }; dialog.Show(); app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle); Check(dialogVm.Loads == 1, "Prism 弹窗宿主和内容共享 ViewModel 时只加载一次"); dialog.Close(); Console.WriteLine("PASS: view and window lifecycle."); return 0; } int exitCode = 0; app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () => { try { if (args.Contains("--status-bar")) { for (int attempt = 0; attempt < 200 && app.MainWindow is not MainWindow; attempt++) { await Task.Delay(50); } MainWindow shell = (MainWindow)app.MainWindow; IStatusBarService status = app.Container.Resolve(); TextBlock text = (TextBlock)shell.FindName("StatusText"); ProgressBar progress = (ProgressBar)shell.FindName("StatusProgress"); Check(text.Text == "就绪" && progress.Visibility == Visibility.Collapsed, "初始状态"); _ = app.Dispatcher.BeginInvoke(new Action(() => throw new System.ComponentModel.DataAnnotations.ValidationException("分页参数无效"))); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); Check(shell.IsVisible && text.Text == "分页参数无效" && status.Current.Level == StatusLevel.Error, "Dispatcher 异常后主窗口继续运行并显示错误"); foreach ((StatusLevel level, string brush, int value) in new[] { (StatusLevel.Info, "PrimaryTextBrush", 0), (StatusLevel.Warning, "WarningBrush", 50), (StatusLevel.Error, "DangerBrush", 75), (StatusLevel.Success, "SuccessBrush", 100) }) { await Task.Run(() => status.Report(level.ToString(), level, value)); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); Check(text.Text == level.ToString() && text.Foreground == shell.FindResource(brush), "状态文字和颜色"); Check(progress.Visibility == Visibility.Visible && progress.Value == value, "任务进度"); } status.Report("完成"); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); Check(text.Text == "完成" && progress.Visibility == Visibility.Collapsed, "纯文本状态隐藏进度"); using var usersClient = new StoreApiClient(new ApiClientOptions { BaseUrl = new Uri("http://localhost:14312/api/v1/") }, httpClient); usersClient.Session.Clear(); var usersExtension = new Prism.Autofac.AutofacContainerExtension(); usersExtension.RegisterInstance(typeof(StoreApiClient), usersClient); usersExtension.FinalizeExtension(); using var usersContainer = usersExtension.Instance; var userService = usersContainer.Resolve(); var usersVm = new UserManagementPageViewModel(userService, app.Container.Resolve(), status); await usersVm.SearchCommand.Execute(); Check(status.Current.Level == StatusLevel.Error && usersVm.CanInteract, "查询失败后显示错误并允许重试"); await usersClient.Auth.LoginAsync(new() { UserName = "admin", Password = "320BB1D43EC042096A83DBAC6DC9294A" }); var usersPage = new UserManagementPage { DataContext = usersVm }; var grid = ((DockPanel)usersPage.Content).Children.OfType().Single(); var sample = new UserItemViewModel { GroupId = 4, SaLevel = UserAdminLevel.SuperAdmin, OemId = "A01", Authority = "在线寄件,商品采集" }; foreach (var (header, expected) in new[] { ("分组", "客服"), ("代理编码", "A01"), ("超管等级", "超级管理员"), ("权限", "在线寄件,商品采集") }) { var column = grid.Columns.OfType().Single(c => Equals(c.Header, header)); var cell = new TextBlock { DataContext = sample }; cell.SetBinding(TextBlock.TextProperty, column.Binding); Check(cell.Text == expected, $"用户列表{header}显示错误:{cell.Text}"); cell.DataContext = new UserItemViewModel { GroupId = 5 }; Check(cell.Text == (header == "分组" ? "5" : ""), $"用户列表{header}历史数据或空值显示错误"); } var groupFilter = LogicalTreeHelper.FindLogicalNode(usersPage, "GroupFilter") as ComboBox; Check(groupFilter?.Items.Count == Enum.GetValues().Length + 1, "用户组包含全部和当前枚举选项"); await app.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); var menu = grid.RowContextMenu; Check(grid.SelectRowOnRightClick && ReferenceEquals(menu, usersPage.Resources["UserRowMenu"]), "使用页面行菜单并选中右键行"); var actions = menu.Items.OfType().Select(item => item.Command).ToArray(); Check(actions.SequenceEqual(new System.Windows.Input.ICommand[] { usersVm.EditCommand, usersVm.BillingCommand, usersVm.DeleteCommand, usersVm.CreateCommand }), "行菜单绑定编辑、删除和创建用户命令"); Check(groupFilter!.Text == "全部", "默认显示全部用户组"); groupFilter.SelectedIndex = 1; Check(usersVm.SelectedGroup == UserGroup.MainAccount, "选择主号更新搜索条件"); Task search = usersVm.SearchCommand.Execute(); Check(!usersVm.CanInteract && status.Current.Level == StatusLevel.Info, "查询期间禁用操作"); await search; Check(usersVm.Users.Single() is { Id: 1, UserName: "admin", Balance: null }, "查询结果映射为列表行"); Check(usersVm.CanInteract && usersVm.Total == 51 && status.Current.Level == StatusLevel.Success, "重试查询成功"); var narrowWindow = new Window { Content = usersPage, Width = 900, Height = 560, ShowInTaskbar = false }; narrowWindow.Show(); narrowWindow.UpdateLayout(); var listScroll = (ScrollViewer)grid.Template.FindName("DG_ScrollViewer", grid); Check(listScroll.ComputedHorizontalScrollBarVisibility == Visibility.Visible && listScroll.ScrollableWidth > 0, "窄窗口显示用户列表横向滚动条"); listScroll.ScrollToRightEnd(); narrowWindow.UpdateLayout(); Check(listScroll.HorizontalOffset > 0 && Math.Abs(listScroll.HorizontalOffset - listScroll.ScrollableWidth) < 1, "用户列表可滚动到最右列"); narrowWindow.Close(); Check(handler.LastGroupId == 3, "查询按主号分组"); groupFilter.SelectedIndex = 0; await usersVm.PageCommand.Execute(new DataGridPageRequest(2, 50)); Check(usersVm.Page == 2 && usersVm.CanInteract && status.Current.Level == StatusLevel.Success, "分页查询成功"); Check(handler.LastGroupId == 3, "翻页保留已查询的用户组"); await usersVm.SearchCommand.Execute(); Check(handler.LastGroupId == null && usersVm.Page == 1, "全部清除分组并返回首页"); var billing = new Store.Management.Views.Dialogs.UserBillingDialogViewModel(userService); billing.OnDialogOpened(new DialogParameters { { "parameter", new UserItemViewModel { Id = 7, UserName = "billing", ShopPointsPerMonth = 45, PostagePerOrder = 3.5m, RefundPostagePerOrder = 4m, GiftOemAmountPerOrder = null, SyncLogisticsPerOrder = 0m } } }); var billingView = new Store.Management.Views.Dialogs.UserBillingDialog { DataContext = billing }; var billingWindow = new Window { Content = billingView, ShowInTaskbar = false }; billingWindow.Show(); billingWindow.UpdateLayout(); Check(billing.Fields.Length == 5 && billing.Fields[0].Value == 45 && billing.Fields[3].EnteredValue == null && billing.Fields[4].EnteredValue == 0, "计费配置回填原值并区分空值和零"); var saveBilling = (Prism.Commands.AsyncDelegateCommand)billing.ConfirmCommand; int beforeBilling = handler.RequestCount; await saveBilling.Execute(); Check(handler.RequestCount == beforeBilling, "未改动计费配置不发送请求"); billing.Fields[0].Value = 29; await saveBilling.Execute(); Check(handler.RequestCount == beforeBilling && billing.ErrorMsg.Length > 0, "拦截无效积分"); billing.Fields[0].Value = 50; var fields = (ItemsControl)billingView.Content; var presenter = (ContentPresenter)fields.ItemContainerGenerator.ContainerFromIndex(3); var title = (MAX.Common.WPF.Controls.TitleContent)System.Windows.Media.VisualTreeHelper.GetChild(presenter, 0); var amount = (HandyControl.Controls.NumericUpDown)title.Content; Check(amount.IsEnabled, "空金额可以直接输入"); var amountInput = (TextBox)amount.Template.FindName("PART_TextBox", amount); amountInput.Focus(); amountInput.SelectAll(); amountInput.SelectedText = "0"; await saveBilling.Execute(); using (var json = System.Text.Json.JsonDocument.Parse(handler.LastUpdateBody!)) { var body = json.RootElement; Check(body.EnumerateObject().Count() == 3 && body.GetProperty("id").GetInt32() == 7 && body.GetProperty("shopPointsPerMonth").GetInt32() == 50 && body.GetProperty("giftOemAmountPerOrder").GetDecimal() == 0, "计费更新请求仅含用户 ID 和两项改动"); } billingWindow.Close(); var editor = new UserEditorDialogViewModel(userService); editor.OnDialogOpened(new DialogParameters { { "user", null } }); var editorView = new UserEditorDialog { DataContext = editor }; editor.Draft.Mobile = "13800000000"; editor.Draft.NickName = "测试"; editor.Group = UserGroup.CustomerService; editor.ParentUserName = "parent"; editor.AgentCode = "agent"; editor.Level = UserAdminLevel.SuperAdmin; editor.Permissions[0].IsSelected = true; var editorPassword = (PasswordBox)editorView.FindName("NewPassword"); var confirmation = (PasswordBox)editorView.FindName("Confirmation"); editorPassword.Password = "secret"; await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute(); Check(editor.ErrorMsg.Contains("不一致"), "密码不一致应阻止保存"); confirmation.Password = "secret"; await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute(); Check(handler.SavedCreate is { UserName: "13800000000", GroupId: UserGroup.CustomerService, ParentUserName: "parent", Authority: "微信回采", OemId: "agent", SaLevel: UserAdminLevel.SuperAdmin }, "新增子账号提交手机号、分组、代理和权限"); editor.OnDialogClosed(); editor = new UserEditorDialogViewModel(userService); editor.OnDialogOpened(new DialogParameters { { "user", new UserItemViewModel { Id = 1, UserName = "old-login", GroupId = 3, Authority = "微信回采,历史权限", Mobile = "13800000000", NickName = "测试" } } }); Check(editor.Permissions[0].IsSelected, "编辑回填权限"); editor.Permissions[0].IsSelected = false; await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute(); Check(handler.SavedUpdate?.Authority == "历史权限" && handler.PasswordTarget == null, "保留未知权限,留空不改密"); editor.UpdatePasswords("new-password", "new-password", ""); await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute(); Check(handler.PasswordTarget == "old-login", "按原登录名修改密码"); await userService.DeleteAsync(1); Console.WriteLine("PASS: status bar bindings, colors and background progress reports."); return; } for (int attempt = 0; attempt < 100 && !app.Windows.OfType().Any(window => window.Content is LoginWindow && window.IsLoaded); attempt++) { await Task.Delay(20); } DialogWindow loginHost = app.Windows.OfType().Single(window => window.Content is LoginWindow); var login = (LoginWindow)loginHost.Content; LoginViewModel loginVm = (LoginViewModel)login.DataContext; if (args.Contains("--cancel-login") || args.Contains("--close-login")) { if (args.Contains("--close-login")) { loginHost.Close(); } else { loginVm.CancelCommand.Execute(null); } Check(!app.Windows.OfType().Any(), "取消登录不得创建主窗口"); Console.WriteLine("PASS: startup login cancellation/window-close shuts down without opening the main window."); return; } var userName = (System.Windows.Controls.TextBox)login.FindName("UserNameBox"); var password = (System.Windows.Controls.PasswordBox)login.FindName("PasswordBox"); var error = (System.Windows.Controls.TextBlock)login.FindName("ErrorText"); foreach (string key in new[] { "PrimaryTextBrush", "SecondaryTextBrush", "RegionBrush", "BorderBrush", "DangerBrush", "ButtonPrimary" }) { Check(login.TryFindResource(key) is not null, $"登录主题资源缺失:{key}"); } Check(loginHost.IsVisible && loginHost.DataContext == login.DataContext, "Prism 弹窗与登录页面共享 ViewModel"); Check(userName.MaxLength == LoginViewModel.InputMaxLength && password.MaxLength == LoginViewModel.InputMaxLength, "登录输入限长"); await loginVm.LoginCommand.Execute(); Check(error.Text == "请输入用户名。", "用户名校验"); userName.Text = " admin "; await loginVm.LoginCommand.Execute(); Check(error.Text == "请输入密码。", "密码校验"); password.Password = "wrong"; await loginVm.LoginCommand.Execute(); Check(error.Text == "用户名或密码错误" && login.IsVisible, "失败后保留登录窗口"); Check(password.Password.Length == 0, "失败后清空密码"); Check(((System.Windows.Controls.Button)login.FindName("LoginButton")).IsEnabled && userName.IsEnabled && password.IsEnabled, "失败后恢复输入和提交"); password.Password = " secret "; Check(ReferenceEquals(((System.Windows.Controls.Button)login.FindName("LoginButton")).Command, loginVm.LoginCommand), "登录按钮绑定 ViewModel 命令"); var pending = loginVm.LoginCommand.Execute(); Check(loginVm.LoadingStatus.IsLoading && !loginVm.CanEdit && !loginVm.LoginCommand.CanExecute(), "登录期间禁用输入与命令"); await loginVm.LoginCommand.Execute(); await pending; Check(handler.RequestCount == 2, "阻止重复提交"); Check(client.Session.IsAuthenticated, "保存登录会话"); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); Check(!loginHost.IsVisible && password.Password.Length == 0, "成功后关闭登录窗口并清空密码"); IDialogService dialogs = app.Container.Resolve(); ButtonResult? closeResult = null; dialogs.Show(nameof(LoginWindow), new DialogParameters(), result => closeResult = result.Result); DialogWindow closingHost = app.Windows.OfType().Single(); LoginWindow closingView = (LoginWindow)closingHost.Content; LoginViewModel closingVm = (LoginViewModel)closingView.DataContext; closingVm.UserName = "admin"; var closingPassword = (System.Windows.Controls.PasswordBox)closingView.FindName("PasswordBox"); closingPassword.Password = " secret "; int lateSuccessCount = 0; closingVm.RequestClose += result => { if (result.Result == ButtonResult.OK) lateSuccessCount++; }; Task closingRequest = closingVm.LoginCommand.Execute(); closingHost.Close(); await closingRequest; Check(closeResult == ButtonResult.None && lateSuccessCount == 0 && closingPassword.Password.Length == 0, "关闭窗口取消登录且不触发迟到成功"); Check(!closingVm.CanEdit && !closingVm.LoginCommand.CanExecute(), "关闭后不可再次登录"); dialogs.Show(nameof(LoginWindow), new DialogParameters(), result => closeResult = result.Result); ((LoginViewModel)app.Windows.OfType().Single().DataContext).CancelCommand.Execute(null); Check(closeResult == ButtonResult.Cancel, "登录取消返回 Prism Cancel"); var page = (FrameworkTestPage)((MainWindow)app.MainWindow).Content; var vm = (FrameworkTestPageViewModel)page.DataContext; vm.TestIocCommand.Execute(); Check(vm.IocResult.StartsWith("通过"), vm.IocResult); vm.ProductName = " "; await vm.PublishNotificationCommand.Execute(); Check(vm.NotificationResult == "请输入商品名称。", "通知输入校验"); vm.ProductName = "通知测试商品"; await vm.PublishNotificationCommand.Execute(); Check(vm.NotificationResult.StartsWith("通知处理完成") && vm.NotificationResult.Contains("缓存模块") && vm.NotificationResult.Contains("审计模块") && vm.NotificationResult.Contains(vm.ProductName), vm.NotificationResult); vm.ShowModelessCommand.Execute(); var dialog = app.Windows.OfType().Single(); var dialogVm = (FrameworkTestDialogViewModel)dialog.DataContext; Check(dialogVm.Message == vm.DialogInput, "Dialog input"); dialogVm.Message = " "; dialogVm.ConfirmCommand.Execute(null); Check(dialog.IsVisible && dialogVm.ErrorMsg.Length > 0, "Empty validation"); dialogVm.Message = "返回测试"; dialogVm.ConfirmCommand.Execute(null); Check(vm.DialogResult == "确认:返回测试", vm.DialogResult); vm.ShowModelessCommand.Execute(); app.Windows.OfType().Single().Close(); Check(vm.DialogResult.Contains("None"), vm.DialogResult); _ = app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => ((FrameworkTestDialogViewModel)app.Windows.OfType().Single().DataContext).CancelCommand.Execute(null))); vm.ShowModalCommand.Execute(); Check(vm.DialogResult.Contains("Cancel"), vm.DialogResult); vm.DialogInput = " "; vm.ShowModalCommand.Execute(); Check(!app.Windows.OfType().Any(), "Empty input blocks dialog"); Console.WriteLine("PASS: startup, view model wiring, IoC, modeless/modal dialogs, parameters, validation, confirm/cancel/window-close."); } catch (Exception ex) { Console.Error.WriteLine(ex); exitCode = 1; } finally { app.Shutdown(); } })); app.Run(); return exitCode; } static void Check(bool success, string message) { if (!success) throw new Exception(message); } } sealed class LifecycleProbe : ViewModelBase { public int FirstLoads { get; private set; } public int Loads { get; private set; } public int Unloads { get; private set; } public int Closes { get; private set; } public bool CancelClose { get; set; } protected override void OnLoadedOnce() { if (Loads != 0) throw new InvalidOperationException("首次加载应先于 OnLoaded"); FirstLoads++; } protected override void OnLoaded() => Loads++; protected override void OnUnloaded() => Unloads++; protected override void OnClosing(System.ComponentModel.CancelEventArgs e) => e.Cancel = CancelClose; protected override void OnClosed() => Closes++; } sealed class LoginHandler : HttpMessageHandler { public UserCreateRequest? SavedCreate { get; private set; } public string? LastUpdateBody { get; private set; } public UserUpdateRequest? SavedUpdate { get; private set; } public string? PasswordTarget { get; private set; } public int? LastGroupId { get; private set; } public int RequestCount { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { RequestCount++; if (request.RequestUri?.AbsolutePath == "/api/v1/user/GetAllUser") { using var query = System.Text.Json.JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken)); LastGroupId = query.RootElement.TryGetProperty("groupId", out var group) && group.ValueKind == System.Text.Json.JsonValueKind.Number ? group.GetInt32() : null; await Task.Delay(30, cancellationToken); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"code":200,"data":{"total":51,"items":[{"id":1,"userName":"admin"}]}}""", Encoding.UTF8, "application/json") }; } string? route = request.RequestUri?.AbsolutePath; if (route is "/api/v1/user/AddSubUser" or "/api/v1/user/UpdateUser" or "/api/v1/user/ChangePassword") { string bodyText = await request.Content!.ReadAsStringAsync(cancellationToken); var options = new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web); if (route.EndsWith("/AddSubUser")) SavedCreate = System.Text.Json.JsonSerializer.Deserialize(bodyText, options); else if (route.EndsWith("/UpdateUser")) { LastUpdateBody = bodyText; SavedUpdate = System.Text.Json.JsonSerializer.Deserialize(bodyText, options); } else PasswordTarget = System.Text.Json.JsonSerializer.Deserialize(bodyText, options)?.UserName; return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"code":200,"data":{"id":1,"userName":"old-login","groupId":3,"authority":"历史权限"}}""", Encoding.UTF8, "application/json") }; } if (request.Method == HttpMethod.Post && request.RequestUri?.PathAndQuery == "/api/v1/user/DelUser?id=1") { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"code":200}""", Encoding.UTF8, "application/json") }; } if (request.RequestUri?.AbsoluteUri != "http://localhost:14312/api/v1/Auth/Login") throw new Exception("登录路由错误"); var body = System.Text.Json.JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken)); if (body.RootElement.GetProperty("userName").GetString() != "admin") throw new Exception("用户名未去除首尾空格"); await Task.Delay(30, cancellationToken); var submittedPassword = body.RootElement.GetProperty("password").GetString()!; if (!System.Text.RegularExpressions.Regex.IsMatch(submittedPassword, "^[0-9A-F]{32}$")) throw new Exception("密码必须为 32 位大写 MD5"); var success = submittedPassword == "320BB1D43EC042096A83DBAC6DC9294A"; return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(success ? "{\"code\":200,\"data\":{\"tokenType\":\"Bearer\",\"token\":\"test-token\",\"expiresInMinutes\":60}}" : "{\"code\":400,\"msg\":\"用户名或密码错误\"}", Encoding.UTF8, "application/json") }; } } sealed class TestApp(StoreApiClient client) : App { protected override void RegisterTypes(IContainerRegistry containerRegistry) { base.RegisterTypes(containerRegistry); containerRegistry.RegisterInstance(client); } }