using System.Diagnostics; using System.IO.Compression; using System.Security.Cryptography; using System.Text.Json; using XFEToolBox.Core.Chat; using XFEToolBox.Core.Tools; using XFEToolBox.Server.Core.Chat; using XFEToolBox.Server.Core.Exceptions; using XFEToolBox.Server.Core.Options; using XFEToolBox.Server.Core.Services; using XFEToolBox.Server.Core.Utilities; using XFEToolBox.Server.Realtime; using XFEToolBox.Server.Services.Chat; using XFEExtension.NetCore.CyberComm; using XFEExtension.NetCore.XFETransform.Json; var tests = new (string Name, Action Run)[] { ("有效源码包可通过校验", ValidPackagePasses), ("路径穿越会被拒绝", PathTraversalIsRejected), ("语义化版本按预期排序", SemanticVersionsAreOrdered), ("文件仓库可保存、查询和下架工具包", RepositoryRoundTripsPackage), ("用户投稿需经管理员审核后才会公开", RepositorySubmissionRequiresReview), ("文件仓库拒绝覆盖已存在的工具版本", RepositoryRejectsDuplicateVersion), ("Socket 传输可安全配置工具下载响应", SocketDownloadResponseIsConfigured), ("系统 CPU 使用率可在负载下被采样", SystemCpuUsageIsMeasuredUnderLoad), ("好友私聊具备权限和消息幂等保证", ChatFriendshipAndMessageAreConsistent), ("公开与私密群聊及邀请卡片按规则工作", ChatGroupsAndInvitationsFollowVisibilityRules), ("聊天请求允许可空分页参数", ChatRequestAllowsNullablePagingValues), ("私密群号具备足够长度并限制账户枚举", ChatGroupNumbersAreStrongAndRateLimited), ("聊天附件分块传输校验完整性和访问权限", ChatAttachmentTransferIsAuthorizedAndVerified), ("实时票据绑定用途且只能消费一次", ChatRealtimeTicketIsAudienceBoundAndSingleUse), ("实时票据限制每用户和全局待使用数量", ChatRealtimeTicketLimitsAreEnforced), ("EdgeOne 回源请求可解析真实客户端 IP", ForwardedClientIpIsResolved) }; var failed = 0; foreach (var test in tests) { try { test.Run(); Console.WriteLine($"[PASS] {test.Name}"); } catch (Exception exception) { failed++; Console.Error.WriteLine($"[FAIL] {test.Name}: {exception}"); } } return failed == 0 ? 0 : 1; static void ValidPackagePasses() { using var package = CreatePackage(); var result = CreateValidator().Inspect(package); Assert(result.Manifest.Id == "base64-generator", "工具 ID 不正确。"); Assert(result.Manifest.RequiresAdministrator, "管理员启动要求没有从 manifest 保留下来。"); Assert(result.Files.Contains("src/Views/Base64Tool.xaml"), "未发现入口 XAML。"); Assert(result.Files.Contains("src/ViewModels/Base64ToolViewModel.cs"), "未发现 ViewModel。"); Assert(result.IconDataUrl?.StartsWith("data:image/png;base64,", StringComparison.Ordinal) == true, "工具图标未被读取。"); } static void PathTraversalIsRejected() { using var package = CreatePackage(archive => AddText(archive, "../outside.cs", "// should be rejected")); try { _ = CreateValidator().Inspect(package); throw new InvalidOperationException("校验器没有拒绝路径穿越文件。"); } catch (ToolPackageValidationException) { } } static void SemanticVersionsAreOrdered() { var versions = new[] { "1.0.0", "1.0.0-beta.2", "2.0.0", "1.0.0-beta.10" }; Array.Sort(versions, SemanticVersionComparer.Instance); Assert( versions.SequenceEqual(["1.0.0-beta.2", "1.0.0-beta.10", "1.0.0", "2.0.0"]), "SemVer 排序结果错误。"); } static void RepositoryRoundTripsPackage() { var root = Path.Combine(Path.GetTempPath(), "XFEToolBox.Server.Test", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); try { var validationOptions = new ToolPackageValidationOptions(); var repository = new FileSystemToolPackageRepository( new ToolPackageValidator(validationOptions), validationOptions, new ToolPackageStorageOptions { StorageRoot = root }); using var packageStream = CreatePackage(); var saved = repository.SaveAsync(packageStream, published: true, overwrite: false).GetAwaiter().GetResult(); Assert(saved.Manifest.Id == "base64-generator", "仓库返回了错误的工具。"); Assert(saved.Sha256.Length == 64, "仓库没有生成有效的 SHA-256。"); var published = repository.ListAsync(publishedOnly: true).GetAwaiter().GetResult(); Assert(published.Count == 1, "已发布工具包没有出现在公开列表中。"); var storedFile = repository.FindFileAsync("base64-generator", "1.0.0", publishedOnly: true).GetAwaiter().GetResult(); Assert(storedFile is not null && File.Exists(storedFile.FullPath), "仓库内的工具包文件不存在。"); _ = repository.SetPublishedAsync("base64-generator", "1.0.0", published: false).GetAwaiter().GetResult(); published = repository.ListAsync(publishedOnly: true).GetAwaiter().GetResult(); Assert(published.Count == 0, "下架后的工具包仍出现在公开列表中。"); } finally { Directory.Delete(root, recursive: true); } } static void RepositoryRejectsDuplicateVersion() { var root = Path.Combine(Path.GetTempPath(), "XFEToolBox.Server.Test", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); try { var validationOptions = new ToolPackageValidationOptions(); var repository = new FileSystemToolPackageRepository( new ToolPackageValidator(validationOptions), validationOptions, new ToolPackageStorageOptions { StorageRoot = root }); using (var firstPackage = CreatePackage()) _ = repository.SaveAsync(firstPackage, published: true, overwrite: false).GetAwaiter().GetResult(); try { using var duplicatePackage = CreatePackage(); _ = repository.SaveAsync(duplicatePackage, published: true, overwrite: true).GetAwaiter().GetResult(); throw new InvalidOperationException("仓库允许 overwrite=true 覆盖已存在的工具版本。"); } catch (ToolPackageConflictException exception) { Assert(exception.Message.Contains("不允许覆盖发布", StringComparison.Ordinal), "重复版本提示不明确。"); } var stored = repository.ListAsync(publishedOnly: false).GetAwaiter().GetResult(); Assert(stored.Count == 1, "拒绝重复版本后,仓库中的版本数量发生了变化。"); } finally { Directory.Delete(root, recursive: true); } } static void RepositorySubmissionRequiresReview() { var root = Path.Combine(Path.GetTempPath(), "XFEToolBox.Server.Test", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); try { var validationOptions = new ToolPackageValidationOptions(); var repository = new FileSystemToolPackageRepository( new ToolPackageValidator(validationOptions), validationOptions, new ToolPackageStorageOptions { StorageRoot = root }); using var packageStream = CreatePackage(); var submitted = repository.SaveSubmissionAsync(packageStream, "user-1", "creator") .GetAwaiter().GetResult(); Assert(!submitted.Published, "用户投稿被直接公开。"); Assert(submitted.ReviewStatus == ToolPackageReviewStatus.Pending, "用户投稿没有进入待审核状态。"); Assert(submitted.SubmittedByUserName == "creator", "投稿账号没有写入元数据。"); Assert(repository.ListAsync(publishedOnly: true).GetAwaiter().GetResult().Count == 0, "待审核投稿出现在公开目录中。"); var approved = repository.SetReviewStatusAsync( submitted.Manifest.Id, submitted.Manifest.Version, ToolPackageReviewStatus.Approved, "admin-1", "administrator").GetAwaiter().GetResult(); Assert(approved.Published && approved.ReviewStatus == ToolPackageReviewStatus.Approved, "管理员通过后工具没有公开。"); Assert(approved.ReviewedByUserName == "administrator" && approved.ReviewedAtUtc.HasValue, "审核人或审核时间没有写入元数据。"); Assert(repository.ListAsync(publishedOnly: true).GetAwaiter().GetResult().Count == 1, "审核通过的工具没有出现在公开目录中。"); var rejected = repository.SetReviewStatusAsync( submitted.Manifest.Id, submitted.Manifest.Version, ToolPackageReviewStatus.Rejected, "admin-1", "administrator", "需要修改说明").GetAwaiter().GetResult(); Assert(!rejected.Published && rejected.ReviewStatus == ToolPackageReviewStatus.Rejected, "管理员拒绝后工具仍处于公开状态。"); Assert(rejected.ReviewMessage == "需要修改说明", "审核说明没有保存。"); } finally { Directory.Delete(root, recursive: true); } } static void SocketDownloadResponseIsConfigured() { var response = new CyberCommHttpResponse(); ServerHttpResponseHelper.ConfigureDownload( response, legacyResponse: null, "application/vnd.xfestudio.xfetool", 1234, "text-encryption-1.0.1.xfetool", "abc123"); Assert( response.Headers["Content-Type"] == "application/vnd.xfestudio.xfetool", "工具包响应类型未设置。"); Assert(response.Headers["Content-Length"] == "1234", "工具包响应长度未设置。"); Assert( response.Headers["Content-Disposition"] == "attachment; filename=\"text-encryption-1.0.1.xfetool\"", "工具包下载文件名未设置。"); Assert(response.Headers["ETag"] == "\"abc123\"", "工具包 ETag 未设置。"); ServerHttpResponseHelper.ConfigureDownload( response: null, legacyResponse: null, "application/vnd.xfestudio.xfetool", 0, "empty.xfetool", "empty"); } static void SystemCpuUsageIsMeasuredUnderLoad() { using var loadStarted = new ManualResetEventSlim(); var loadTask = Task.Factory.StartNew(() => { loadStarted.Set(); var end = Stopwatch.GetTimestamp() + (long)(Stopwatch.Frequency * 1.2); var value = 1d; while (Stopwatch.GetTimestamp() < end) value = Math.Sqrt(value + 1.000001); GC.KeepAlive(value); }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); loadStarted.Wait(); var usage = SystemCpuUsageSampler.SampleAsync(TimeSpan.FromMilliseconds(700)) .GetAwaiter().GetResult(); loadTask.GetAwaiter().GetResult(); Assert(double.IsFinite(usage), "CPU 使用率不是有效数值。"); Assert(usage is >= 0 and <= 100, $"CPU 使用率超出范围:{usage:F2}%"); Assert(usage > 0.05, $"制造 CPU 负载后采样结果仍为 {usage:F2}%。"); Console.WriteLine($" 负载采样结果:{usage:F2}%"); } static void ChatFriendshipAndMessageAreConsistent() => WithChatRepository((repository, attachmentRoot) => { var request = repository.CreateFriendRequestAsync("alice", "bob", "hello").GetAwaiter().GetResult(); _ = repository.RespondToFriendRequestAsync(request.Id, "bob", accept: true).GetAwaiter().GetResult(); Assert(repository.AreFriends("alice", "bob"), "接受申请后没有建立好友关系。"); var conversation = repository.GetOrCreateDirectConversationAsync("alice", "bob").GetAwaiter().GetResult(); var firstWrite = repository.SendMessageWithResultAsync( conversation.Id, "alice", ChatMessageType.Text, "第一条消息", null, "client-message-1", null) .GetAwaiter().GetResult(); var retriedWrite = repository.SendMessageWithResultAsync( conversation.Id, "alice", ChatMessageType.Text, "第一条消息", null, "client-message-1", null) .GetAwaiter().GetResult(); var first = firstWrite.Message; var retried = retriedWrite.Message; Assert(firstWrite.Created && !retriedWrite.Created, "消息写入没有区分首次创建与幂等重试。"); Assert(first.Id == retried.Id && first.Sequence == retried.Sequence, "重试发送生成了重复消息。"); var history = repository.GetMessageHistoryAsync(conversation.Id, "bob").GetAwaiter().GetResult(); Assert(history.Items.Count == 1 && history.Items[0].Text == "第一条消息", "好友未能读取私聊历史。"); var exception = AssertChatThrows(() => repository.SendMessageAsync( conversation.Id, "mallory", ChatMessageType.Text, "unauthorized", null, "mallory-1", null) .GetAwaiter().GetResult()); Assert(exception.Error == ChatRepositoryError.Forbidden, "非会话用户发送消息未被权限层拒绝。"); Assert(repository.DeleteFriendshipAsync("alice", "bob").GetAwaiter().GetResult(), "测试好友关系删除失败。"); var replayException = AssertChatThrows(() => repository.SendMessageWithResultAsync( conversation.Id, "alice", ChatMessageType.Text, "第一条消息", null, "client-message-1", null) .GetAwaiter().GetResult()); Assert(replayException.Error == ChatRepositoryError.Forbidden, "删除好友后仍可通过幂等键重放旧消息。"); GC.KeepAlive(attachmentRoot); }); static void ChatGroupsAndInvitationsFollowVisibilityRules() => WithChatRepository((repository, attachmentRoot) => { var friendRequest = repository.CreateFriendRequestAsync("alice", "bob", null).GetAwaiter().GetResult(); _ = repository.RespondToFriendRequestAsync(friendRequest.Id, "bob", accept: true).GetAwaiter().GetResult(); var publicGroup = repository.CreateGroupAsync( "alice", "公开讨论组", "public", ChatGroupVisibility.Public).GetAwaiter().GetResult(); var privateGroup = repository.CreateGroupAsync( "alice", "私密讨论组", "private", ChatGroupVisibility.Private).GetAwaiter().GetResult(); Assert(privateGroup.GroupNumber.Length == ChatRepository.GroupNumberLength && privateGroup.GroupNumber.All(char.IsAsciiDigit), "群号不是 12 位安全随机数字。"); var recommended = repository.GetRecommendedGroupsAsync("bob", null) .WaitAsync(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult(); Assert(recommended.Any(group => group.Id == publicGroup.Id), "公开群没有出现在大厅推荐中。"); Assert(recommended.All(group => group.Id != privateGroup.Id), "私密群泄露到了大厅推荐中。"); var newGroupHistory = repository.GetMessageHistoryAsync( publicGroup.ConversationId, "alice", beforeSequence: null, limit: 50) .WaitAsync(TimeSpan.FromSeconds(2)).GetAwaiter().GetResult(); Assert(newGroupHistory.Items.Count == 0 && !newGroupHistory.HasMore && newGroupHistory.NextBeforeSequence is null, "新建群聊的首屏空历史没有正常返回。"); var exactLookup = repository.FindGroupByNumberAsync(privateGroup.GroupNumber, "bob").GetAwaiter().GetResult(); Assert(exactLookup?.Id == privateGroup.Id, "输入准确群号无法找到私密群。"); var invitation = repository.InviteFriendToGroupAsync( privateGroup.Id, "alice", "bob", "欢迎加入").GetAwaiter().GetResult(); var invitationCard = repository.FindMessageByGroupInvitationIdAsync(invitation.Id).GetAwaiter().GetResult(); Assert(invitationCard?.MessageType == ChatMessageType.GroupInvitation && invitationCard.GroupInvitationId == invitation.Id, "邀请好友时没有生成群邀请卡片。"); _ = repository.RespondToGroupInvitationAsync(invitation.Id, "bob", accept: true).GetAwaiter().GetResult(); Assert(repository.IsGroupMember(privateGroup.Id, "bob"), "接受群邀请后没有加入群聊。"); var groupMessage = repository.SendMessageAsync( privateGroup.ConversationId, "bob", ChatMessageType.Text, "群消息", null, "group-message-1", null) .GetAwaiter().GetResult(); Assert(groupMessage.Sequence > 0, "群成员无法发送群消息。"); GC.KeepAlive(attachmentRoot); }); static void ChatRequestAllowsNullablePagingValues() { var json = XFEJson.Parse(""" { "beforeSequence": null, "limit": 50, "offset": null, "enabled": true, "invalidNumber": "not-a-number" } """); Assert(ChatRequestValueReader.GetInt64(json["beforeSequence"]) is null, "显式 JSON null 被错误解析为非空 Int64。"); Assert(ChatRequestValueReader.GetInt32(json["offset"]) is null, "显式 JSON null 被错误解析为非空 Int32。"); Assert(ChatRequestValueReader.GetInt32(json["limit"]) == 50, "有效分页数量没有被正确解析。"); Assert(ChatRequestValueReader.GetBoolean(json["enabled"]) == true, "有效布尔参数没有被正确解析。"); Assert(ChatRequestValueReader.GetInt64(json["invalidNumber"]) is null, "类型错误的分页参数没有安全地返回空值。"); Assert(ChatRequestValueReader.GetInt64(json["missing"]) is null, "缺失的分页参数没有安全地返回空值。"); } static void ChatGroupNumbersAreStrongAndRateLimited() => WithChatRepository((repository, attachmentRoot) => { var group = repository.CreateGroupAsync( "alice", "限流测试群", null, ChatGroupVisibility.Private).GetAwaiter().GetResult(); Assert(group.GroupNumber.Length == ChatRepository.GroupNumberLength && group.GroupNumber.All(char.IsAsciiDigit), "新建群号没有使用至少 12 位数字。"); for (var attempt = 0; attempt < ChatRepository.DefaultMaxGroupNumberAttemptsPerMinute; attempt++) Assert(repository.FindGroupByNumberAsync("000000000000", "bob").GetAwaiter().GetResult() is null, "不存在的群号被错误命中。"); var exception = AssertChatThrows(() => repository.JoinGroupByNumberAsync(group.GroupNumber, "bob") .GetAwaiter().GetResult()); Assert(exception.Error == ChatRepositoryError.RateLimited, "群号查询与加入没有共享账户级限流。"); GC.KeepAlive(attachmentRoot); }); static void ChatAttachmentTransferIsAuthorizedAndVerified() => WithChatRepository((repository, attachmentRoot) => { for (var attempt = 0; attempt < 32; attempt++) { var unknown = Guid.NewGuid().ToString("N"); var unknownException = AssertChatThrows(() => repository.AppendAttachmentChunkAsync( unknown, "alice", 0, new byte[] { 1 }).GetAwaiter().GetResult()); Assert(unknownException.Error == ChatRepositoryError.NotFound, "未知附件 ID 没有返回 NotFound。"); } Assert(repository.ActiveAttachmentLockCount == 0, "随机附件 ID 在 keyed-lock 表中留下了永久项。"); var request = repository.CreateFriendRequestAsync("alice", "bob", null).GetAwaiter().GetResult(); _ = repository.RespondToFriendRequestAsync(request.Id, "bob", accept: true).GetAwaiter().GetResult(); var conversation = repository.GetOrCreateDirectConversationAsync("alice", "bob").GetAwaiter().GetResult(); var content = "chunked-chat-attachment"u8.ToArray(); var hash = Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant(); var attachment = repository.InitializeAttachmentAsync( "alice", "../sample.txt", "text/plain", content.Length, hash).GetAwaiter().GetResult(); Assert(attachment.FileName == "sample.txt", "附件文件名没有移除路径信息。"); _ = repository.AppendAttachmentChunkAsync( attachment.Id, "alice", 0, content.AsMemory(0, 7)).GetAwaiter().GetResult(); _ = repository.AppendAttachmentChunkAsync( attachment.Id, "alice", 7, content.AsMemory(7)).GetAwaiter().GetResult(); var completed = repository.CompleteAttachmentAsync(attachment.Id, "alice", hash).GetAwaiter().GetResult(); Assert(completed.Status == ChatAttachmentStatus.Complete && completed.Sha256 == hash, "附件完成状态或 SHA-256 不正确。"); Assert(repository.ActiveAttachmentLockCount == 0, "附件完成后 keyed-lock 没有回收。"); _ = repository.SendMessageAsync( conversation.Id, "alice", ChatMessageType.File, string.Empty, attachment.Id, "file-message-1", null) .GetAwaiter().GetResult(); var downloaded = repository.DownloadAttachmentChunkAsync( attachment.Id, "bob", 0, content.Length).GetAwaiter().GetResult(); Assert(downloaded.Data.SequenceEqual(content) && downloaded.EndOfFile, "好友下载到的附件内容不一致。"); var exception = AssertChatThrows(() => repository.DownloadAttachmentChunkAsync( attachment.Id, "mallory", 0, content.Length).GetAwaiter().GetResult()); Assert(exception.Error == ChatRepositoryError.Forbidden, "无关用户能够下载会话附件。"); var fullAttachmentRoot = Path.GetFullPath(attachmentRoot) + Path.DirectorySeparatorChar; Assert(Directory.EnumerateFiles(attachmentRoot, "*", SearchOption.AllDirectories) .All(path => Path.GetFullPath(path).StartsWith(fullAttachmentRoot, StringComparison.OrdinalIgnoreCase)), "附件文件写到了存储根目录之外。"); }); static void ChatRealtimeTicketIsAudienceBoundAndSingleUse() { var store = new ChatRealtimeTicketStore(ticketLifetime: TimeSpan.FromSeconds(30)); var issued = store.Issue("alice", "alice", "Alice", "test-device", sessionId: "session-alice"); Assert(store.TryConsume( issued.Ticket, ChatRealtimeTicketStore.RealtimeAudience, ChatRealtimeTicketStore.RealtimeChannel, out var claims), "有效实时票据无法消费。"); Assert(claims.UserId == "alice", "实时票据没有绑定登录用户。"); Assert(claims.SessionId == "session-alice", "实时票据没有绑定来源登录会话。"); Assert(!store.TryConsume( issued.Ticket, ChatRealtimeTicketStore.RealtimeAudience, ChatRealtimeTicketStore.RealtimeChannel, out _), "实时票据被重复消费。"); var wrongAudience = store.Issue("bob", "bob", "Bob", "test-device"); Assert(!store.TryConsume( wrongAudience.Ticket, "another.audience", ChatRealtimeTicketStore.RealtimeChannel, out _), "实时票据接受了错误用途。"); Assert(!store.TryConsume( wrongAudience.Ticket, ChatRealtimeTicketStore.RealtimeAudience, ChatRealtimeTicketStore.RealtimeChannel, out _), "用途不匹配的票据没有立即失效。"); } static void ChatRealtimeTicketLimitsAreEnforced() { var store = new ChatRealtimeTicketStore( ticketLifetime: TimeSpan.FromSeconds(30), maximumOutstandingTickets: 3, maximumOutstandingTicketsPerUser: 2); var aliceOne = store.Issue("alice", "alice", "Alice", "device-1"); _ = store.Issue("alice", "alice", "Alice", "device-2"); try { _ = store.Issue("alice", "alice", "Alice", "device-3"); throw new InvalidOperationException("每用户票据上限没有生效。"); } catch (ChatRealtimeTicketLimitException) { } _ = store.Issue("bob", "bob", "Bob", "device-1"); try { _ = store.Issue("mallory", "mallory", "Mallory", "device-1"); throw new InvalidOperationException("全局票据上限没有生效。"); } catch (ChatRealtimeTicketLimitException) { } Assert(store.TryConsume( aliceOne.Ticket, ChatRealtimeTicketStore.RealtimeAudience, ChatRealtimeTicketStore.RealtimeChannel, out _), "配额中的有效票据无法消费。"); _ = store.Issue("mallory", "mallory", "Mallory", "device-1"); Assert(store.OutstandingTicketCount == 3, "消费票据后没有释放全局配额。"); } static void ForwardedClientIpIsResolved() { Assert( ForwardedClientIpResolver.Resolve( "10.0.0.8", "203.0.113.21", "198.51.100.7, 203.0.113.20") == "203.0.113.21", "没有优先使用 EdgeOne 提供的 EO-Connecting-IP。"); Assert( ForwardedClientIpResolver.Resolve( "10.0.0.8", null, "198.51.100.7, 203.0.113.20") == "203.0.113.20", "没有使用 X-Forwarded-For 中 EdgeOne 追加的最右侧地址。"); Assert( ForwardedClientIpResolver.Resolve( "::ffff:192.0.2.10", "not-an-ip", "also-invalid") == "192.0.2.10", "非法转发请求头没有回退到连接 IP。"); Assert( ForwardedClientIpResolver.Resolve( "192.0.2.10", null, "198.51.100.7, not-an-ip") == "192.0.2.10", "X-Forwarded-For 最右侧值无效时不应信任左侧可伪造地址。"); } static void WithChatRepository(Action test) { var root = Path.Combine(Path.GetTempPath(), "XFEToolBox.Server.Chat.Test", Guid.NewGuid().ToString("N")); var attachmentRoot = Path.Combine(root, "attachments"); Directory.CreateDirectory(root); try { using var repository = new ChatRepository( Path.Combine(root, "chat.db"), attachmentRoot, maxAttachmentBytes: 8 * 1024 * 1024, maxAttachmentChunkBytes: 64 * 1024); repository.UserExists = userId => userId is "alice" or "bob" or "mallory"; repository.InitializeAsync().GetAwaiter().GetResult(); test(repository, attachmentRoot); } finally { if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } static ChatRepositoryException AssertChatThrows(Action action) { try { action(); throw new InvalidOperationException("预期聊天仓储拒绝操作,但操作成功了。"); } catch (ChatRepositoryException exception) { return exception; } } static ToolPackageValidator CreateValidator() => new(new ToolPackageValidationOptions()); static MemoryStream CreatePackage(Action? customize = null) { var stream = new MemoryStream(); using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) { var manifest = new ToolPackageManifest { Id = "base64-generator", Name = "Base64 生成器", Version = "1.0.0", Description = "测试工具包", Author = "XFEstudio", Icon = "assets/icon.png", Category = "编码", Tags = ["base64"], MinimumHostVersion = "0.2.0", RequiresAdministrator = true, Entry = new ToolEntryManifest { ViewXaml = "src/Views/Base64Tool.xaml", ViewClass = "XFEToolBox.Tools.Base64.Views.Base64Tool", ViewCodeBehind = "src/Views/Base64Tool.xaml.cs", ViewModel = "src/ViewModels/Base64ToolViewModel.cs", ViewModelClass = "XFEToolBox.Tools.Base64.ViewModels.Base64ToolViewModel" } }; AddText(archive, "manifest.json", JsonSerializer.Serialize(manifest, new JsonSerializerOptions(JsonSerializerDefaults.Web))); AddText(archive, "src/Views/Base64Tool.xaml", """ """); AddText(archive, "src/Views/Base64Tool.xaml.cs", "namespace XFEToolBox.Tools.Base64.Views; public sealed class Base64Tool { }"); AddText(archive, "src/ViewModels/Base64ToolViewModel.cs", "namespace XFEToolBox.Tools.Base64.ViewModels; public sealed class Base64ToolViewModel { }"); AddBytes(archive, "assets/icon.png", Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")); customize?.Invoke(archive); } stream.Position = 0; return stream; } static void AddText(ZipArchive archive, string path, string content) { var entry = archive.CreateEntry(path, CompressionLevel.Fastest); using var writer = new StreamWriter(entry.Open()); writer.Write(content); } static void AddBytes(ZipArchive archive, string path, byte[] content) { var entry = archive.CreateEntry(path, CompressionLevel.Fastest); using var stream = entry.Open(); stream.Write(content); } static void Assert(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }