Files
Store.Management/Tests/Store.Management.App.SmokeTests/Program.cs
T
2026-11-13 15:51:16 +08:00

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);
}
}