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 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); var app = new App(client); app.InitializeComponent(); int exitCode = 0; app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () => { try { 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 LoginHandler : HttpMessageHandler { public int RequestCount { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { RequestCount++; 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") }; } }