Files
Store.Management/Tests/Store.Management.App.SmokeTests/Program.cs
T
2026-11-12 18:22:14 +08:00

261 lines
16 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 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") ? new App() : new TestApp(client);
app.InitializeComponent();
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, "初始状态");
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 usersVm = new UserManagementPageViewModel(usersClient, 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" });
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, "重试查询成功");
await usersVm.PageCommand.Execute(new DataGridPageRequest(2, 50));
Check(usersVm.Page == 2 && usersVm.CanInteract && status.Current.Level == StatusLevel.Success, "分页查询成功");
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 LoginHandler : HttpMessageHandler
{
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")
{
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")
};
}
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);
}
}