XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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<ChatRepository, string> 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<ZipArchive>? 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", """
            <UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         x:Class="XFEToolBox.Tools.Base64.Views.Base64Tool">
                <Grid />
            </UserControl>
            """);
        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);
}