RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架
大石头 authored at 2022-08-10 13:26:19
6.40 KiB
NewLife.Remoting
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NewLife;
using NewLife.Data;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Security;
using XCode.Membership;
using Xunit;

namespace XUnitTest.Samples;

/// <summary>ZeroRpcServer 多协议集成测试。覆盖 TCP / UDP / WebSocket / HTTP 四种协议</summary>
/// <remarks>
/// 四个测试方法共享同一 ApiServer 实例(IClassFixture),减少启动开销。
/// 每个协议独立建立连接,Session 状态各自隔离,互不干扰。
/// </remarks>
[Collection("ZeroRpcServer")]
[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
public class ZeroRpcServerIntegrationTests : IClassFixture<ZeroRpcServerFixture>
{
    private readonly ZeroRpcServerFixture _fixture;

    public ZeroRpcServerIntegrationTests(ZeroRpcServerFixture fixture) => _fixture = fixture;

    #region 辅助:通用断言逻辑
    /// <summary>
    /// 对给定客户端执行一整套 RPC 断言流程:
    /// api/all、My/Add、My/RC4、User/FindByID(首次成功 + 第二次 429)
    /// </summary>
    private static async Task AssertRpcCallsAsync(IApiClient client, Int32 userId)
    {
        // 1. api/all —— 返回接口列表,包含我们注册的接口
        var apis = await client.InvokeAsync<String[]>("Api/All");
        Assert.NotNull(apis);
        Assert.True(apis.Length >= 5, $"接口数量应 ≥ 5,实际:{apis.Length}");
        Assert.Contains(apis, a => a.Contains("My/Add"));
        Assert.Contains(apis, a => a.Contains("My/RC4"));
        Assert.Contains(apis, a => a.Contains("User/FindByID"));

        // 2. api/info —— 返回服务端信息,含 MachineName
        var state = Rand.NextString(8);
        var state2 = Rand.NextString(8);
        var info = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state, state2 });
        Assert.NotNull(info);
        Assert.True(info.ContainsKey("MachineName"), "info 应包含 MachineName");
        Assert.True(info.ContainsKey("State"), "info 应包含 State");
        Assert.Equal(state, info["State"]?.ToString());

        // 3. My/Add —— 整数加法,精确验证返回值
        var sum = await client.InvokeAsync<Int32>("My/Add", new { x = 13, y = 7 });
        Assert.Equal(20, sum);

        var sum2 = await client.InvokeAsync<Int32>("My/Add", new { x = -5, y = 5 });
        Assert.Equal(0, sum2);

        // 4. My/RC4 —— 二进制往返:原文加密后再次加密应还原
        // 注: RC4 服务返回 IPacket 二进制,必须用 InvokeAsync<IPacket>(ApiClient 直接返回 message.Data,不过 JSON 解码)
        var original = "Hello NewLife RC4".GetBytes();
        var pk1 = (ArrayPacket)original;
        var encrypted = await client.InvokeAsync<IPacket>("My/RC4", pk1);
        Assert.NotNull(encrypted);
        var encryptedBytes = encrypted.ToArray();
        Assert.NotEqual(original, encryptedBytes); // 加密后应不同

        var pk2 = (ArrayPacket)encryptedBytes;
        var decrypted = await client.InvokeAsync<IPacket>("My/RC4", pk2);
        var decryptedBytes = decrypted?.ToArray();
        Assert.Equal(original, decryptedBytes);    // 解密后应还原

        // 5. User/FindByID(首次)—— 返回实体,校验字段
        var user = await client.InvokeAsync<IDictionary<String, Object>>("User/FindByID", new { id = userId });
        Assert.NotNull(user);
        Assert.True(user.ContainsKey("Name"), "User 响应应包含 Name 字段");
        Assert.False(user["Name"]?.ToString().IsNullOrEmpty(), "User.Name 不应为空");

        // 6. User/FindByID(第二次同 Session)—— 触发 429 TooManyRequests
        var ex = await Assert.ThrowsAsync<ApiException>(
            () => client.InvokeAsync<IDictionary<String, Object>>("User/FindByID", new { id = userId }));
        Assert.Equal(429, ex.Code);
        Assert.Contains("调用次数过多", ex.Message);
    }
    #endregion

    #region TCP 协议测试
    [Fact(DisplayName = "TCP协议_完整RPC调用链")]
    public async Task TcpProtocolTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_fixture.Port}")
        {
            Log = XTrace.Log,
        };

        await AssertRpcCallsAsync(client, _fixture.UserId);
    }
    #endregion

    #region UDP 协议测试
    [Fact(DisplayName = "UDP协议_完整RPC调用链")]
    public async Task UdpProtocolTest()
    {
        using var client = new ApiClient($"udp://127.0.0.1:{_fixture.Port}")
        {
            Log = XTrace.Log,
        };

        await AssertRpcCallsAsync(client, _fixture.UserId);
    }
    #endregion

    #region WebSocket 协议测试
    [Fact(DisplayName = "WebSocket协议_完整RPC调用链")]
    public async Task WebSocketProtocolTest()
    {
        using var client = new ApiClient($"ws://127.0.0.1:{_fixture.Port}")
        {
            Log = XTrace.Log,
        };

        await AssertRpcCallsAsync(client, _fixture.UserId);
    }
    #endregion

    #region HTTP 协议测试
    [Fact(DisplayName = "HTTP协议_完整RPC调用链")]
    public async Task HttpProtocolTest()
    {
        var client = new ApiHttpClient($"http://127.0.0.1:{_fixture.Port}")
        {
            Log = XTrace.Log,
        };

        // 1. api/all(GET):返回完整签名格式 "ReturnType Route(params)",用 Contains 子串匹配
        var apis = await client.GetAsync<String[]>("Api/All");
        Assert.NotNull(apis);
        Assert.True(apis.Length >= 5, $"接口数量应 ≥ 5,实际:{apis.Length}");
        Assert.Contains(apis, a => a.Contains("My/Add"));

        // 2. api/info(POST),校验 State 字段回显
        var state = Rand.NextString(8);
        var state2 = Rand.NextString(8);
        var info = await client.PostAsync<IDictionary<String, Object>>("Api/Info", new { state, state2 });
        Assert.NotNull(info);
        Assert.Equal(state, info["State"]?.ToString());

        // 3. My/Add(GET:名称含 Get 或参数为基础类型时自动转 GET)
        var sum = await client.InvokeAsync<Int32>("My/Add", new { x = 100, y = 200 }, default);
        Assert.Equal(300, sum);

        // 4. User/FindByID(GET)
        var user = await client.GetAsync<IDictionary<String, Object>>("User/FindByID", new { id = _fixture.UserId });
        Assert.NotNull(user);
        Assert.True(user.ContainsKey("Name"));
        Assert.False(user["Name"]?.ToString().IsNullOrEmpty());
    }
    #endregion
}