472 lines
31 KiB
C#
472 lines
31 KiB
C#
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<StoreApiClient>();
|
|
Check(ReferenceEquals(productionClient, productionContainer.Resolve<StoreApiClient>()), "自动注册 API 单例");
|
|
Check(productionContainer.Resolve<Store.Management.Framework.Services.ILoginService>() != null, "自动注册登录服务");
|
|
}
|
|
var testExtension = new Prism.Autofac.AutofacContainerExtension();
|
|
testExtension.RegisterInstance(typeof(StoreApiClient), client);
|
|
testExtension.FinalizeExtension();
|
|
using var container = testExtension.Instance;
|
|
Check(ReferenceEquals(client, container.Resolve<StoreApiClient>()), "保留测试客户端注册");
|
|
var service = container.Resolve<Store.Management.Framework.Services.ILoginService>();
|
|
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<IStatusBarService>();
|
|
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<IUserService>();
|
|
var usersVm = new UserManagementPageViewModel(userService, app.Container.Resolve<IDialogService>(), 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<DataGridEx>().Single();
|
|
var sample = new UserInfo { GroupId = 4, SaLevel = UserAdminLevel.SuperAdmin, OemId = "A01", Authority = "在线寄件,商品采集" };
|
|
foreach (var (header, expected) in new[] { ("分组", "客服"), ("代理编码", "A01"), ("超管等级", "超级管理员"), ("权限", "在线寄件,商品采集") })
|
|
{
|
|
var column = grid.Columns.OfType<DataGridTextColumn>().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 UserInfo { GroupId = 5 };
|
|
Check(cell.Text == (header == "分组" ? "5" : ""), $"用户列表{header}历史数据或空值显示错误");
|
|
}
|
|
var groupFilter = LogicalTreeHelper.FindLogicalNode(usersPage, "GroupFilter") as ComboBox;
|
|
Check(groupFilter?.Items.Count == Enum.GetValues<UserGroup>().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<MenuItem>().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 == Store.Management.Api.Users.UserGroup.MainAccount, "选择主号更新搜索条件");
|
|
Task search = usersVm.SearchCommand.Execute();
|
|
Check(!usersVm.CanInteract && status.Current.Level == StatusLevel.Info, "查询期间禁用操作");
|
|
await search;
|
|
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 UserInfo
|
|
{
|
|
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 UserInfo { 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<DialogWindow>().Any(window => window.Content is LoginWindow && window.IsLoaded); attempt++)
|
|
{
|
|
await Task.Delay(20);
|
|
}
|
|
DialogWindow loginHost = app.Windows.OfType<DialogWindow>().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<MainWindow>().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<IDialogService>();
|
|
ButtonResult? closeResult = null;
|
|
dialogs.Show(nameof(LoginWindow), new DialogParameters(), result => closeResult = result.Result);
|
|
DialogWindow closingHost = app.Windows.OfType<DialogWindow>().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<DialogWindow>().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<DialogWindow>().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<DialogWindow>().Single().Close();
|
|
Check(vm.DialogResult.Contains("None"), vm.DialogResult);
|
|
_ = app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
|
|
((FrameworkTestDialogViewModel)app.Windows.OfType<DialogWindow>().Single().DataContext).CancelCommand.Execute(null)));
|
|
vm.ShowModalCommand.Execute();
|
|
Check(vm.DialogResult.Contains("Cancel"), vm.DialogResult);
|
|
vm.DialogInput = " ";
|
|
vm.ShowModalCommand.Execute();
|
|
Check(!app.Windows.OfType<DialogWindow>().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<HttpResponseMessage> 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<UserCreateRequest>(bodyText, options);
|
|
else if (route.EndsWith("/UpdateUser"))
|
|
{
|
|
LastUpdateBody = bodyText;
|
|
SavedUpdate = System.Text.Json.JsonSerializer.Deserialize<UserUpdateRequest>(bodyText, options);
|
|
}
|
|
else
|
|
PasswordTarget = System.Text.Json.JsonSerializer.Deserialize<ChangePasswordRequest>(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);
|
|
}
|
|
}
|