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

namespace XUnitTest.Integration;

/// <summary>Token传递与登录流程集成测试</summary>
/// <remarks>
/// 验证 ApiClient 的 Token 注入、TokenApiHandler 的令牌会话管理,
/// 以及 OnLoginAsync / OnNewSession 自动登录机制。
/// </remarks>
public class TokenLoginIntegrationTests : DisposeBase
{
    private readonly ApiServer _Server;
    private readonly Int32 _Port;

    public TokenLoginIntegrationTests()
    {
        _Server = new ApiServer(0)
        {
            Log = XTrace.Log,
            ShowError = true,
        };
        _Server.Handler = new TokenApiHandler { Host = _Server };
        _Server.Start();

        _Port = _Server.Port;
    }

    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);
        _Server.TryDispose();
    }

    #region Token注入
    [Fact(DisplayName = "Token_自动注入到请求参数")]
    public async Task TokenAutoInjectTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_Port}")
        {
            Token = "MyToken123",
        };

        // Token 通过 Api/Info 的返回值 token 字段验证(TokenApiHandler 从参数中提取 Token 并设置到会话)
        var infs = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "test" });
        Assert.NotNull(infs);
        Assert.Equal("MyToken123", infs["token"]?.ToString());
    }

    [Fact(DisplayName = "Token_无Token时token字段为空")]
    public async Task NoTokenTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_Port}");

        var infs = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "test" });
        Assert.NotNull(infs);
        // 无 Token 时,token 字段应为空
        var token = infs.TryGetValue("token", out var t) ? t?.ToString() : null;
        Assert.True(token.IsNullOrEmpty());
    }

    [Fact(DisplayName = "Token_动态更改Token")]
    public async Task DynamicTokenChangeTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_Port}")
        {
            Token = "Token1",
        };

        var infs = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "test1" });
        Assert.NotNull(infs);
        Assert.Equal("Token1", infs["token"]?.ToString());

        // 更换 Token
        client.Token = "Token2";
        infs = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "test2" });
        Assert.NotNull(infs);
        Assert.Equal("Token2", infs["token"]?.ToString());
    }
    #endregion

    #region 会话Token共享
    [Fact(DisplayName = "Token_会话Token正确设置")]
    public async Task SessionTokenSetTest()
    {
        var token = "TestToken_" + Guid.NewGuid().ToString("N").Substring(0, 8);

        using var client = new ApiClient($"tcp://127.0.0.1:{_Port}") { Token = token };

        // 通过 Api/Info 验证 Token 正确设置
        var infs = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "verify" });
        Assert.NotNull(infs);
        Assert.Equal(token, infs["token"]?.ToString());

        // 同一连接再次调用,Token 仍然保留
        infs = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "verify2" });
        Assert.NotNull(infs);
        Assert.Equal(token, infs["token"]?.ToString());
    }

    [Fact(DisplayName = "Token_不同客户端Token隔离")]
    public async Task IsolatedTokenTest()
    {
        using var client1 = new ApiClient($"tcp://127.0.0.1:{_Port}") { Token = "TokenA_123" };
        using var client2 = new ApiClient($"tcp://127.0.0.1:{_Port}") { Token = "TokenB_456" };

        var infs1 = await client1.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "a" });
        var infs2 = await client2.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "b" });

        Assert.Equal("TokenA_123", infs1["token"]?.ToString());
        Assert.Equal("TokenB_456", infs2["token"]?.ToString());
    }
    #endregion

    #region 自动登录
    [Fact(DisplayName = "自动登录_OnNewSession触发登录")]
    public async Task AutoLoginOnNewSessionTest()
    {
        LoginClient.LoginCalledCount = 0;

        using var client = new LoginClient
        {
            Servers = [$"tcp://127.0.0.1:{_Port}"],
        };

        // 首次调用会触发连接,连接后触发 OnNewSession → OnLoginAsync
        var result = await client.InvokeAsync<String[]>("Api/All");
        Assert.NotNull(result);

        // 等待异步登录完成
        for (var i = 0; i < 50 && LoginClient.LoginCalledCount == 0; i++)
        {
            await Task.Delay(50);
        }

        Assert.True(LoginClient.LoginCalledCount >= 1, "OnLoginAsync 应被调用至少1次");
    }

    [Fact(DisplayName = "LoginAsync_手动触发登录")]
    public async Task ManualLoginAsyncTest()
    {
        LoginClient.LoginCalledCount = 0;

        using var client = new LoginClient
        {
            Servers = [$"tcp://127.0.0.1:{_Port}"],
        };

        // Open() 确保集群初始化,LoginAsync 内部通过 Cluster 获取连接
        client.Open();

        // 手动登录
        var result = await client.LoginAsync();
        Assert.True(LoginClient.LoginCalledCount >= 1, "手动 LoginAsync 应被调用");
    }
    #endregion

    #region HTTP模式Token
    [Fact(DisplayName = "HTTP模式_Token通过请求头传递")]
    public async Task HttpTokenViaHeaderTest()
    {
        // 使用HTTP模式创建服务器
        using var httpServer = new ApiServer(new NetUri(NetType.Http, "*", 0))
        {
            Log = XTrace.Log,
            ShowError = true,
        };
        httpServer.Handler = new TokenApiHandler { Host = httpServer };
        httpServer.Start();

        IApiClient client = new ApiHttpClient($"http://127.0.0.1:{httpServer.Port}")
        {
            Token = "HttpToken456",
        };
        using var _ = client as IDisposable;

        var result = await client.InvokeAsync<IDictionary<String, Object>>("Api/Info", new { state = "test" });
        Assert.NotNull(result);
        Assert.Equal("HttpToken456", result["token"]?.ToString());
    }
    #endregion

    #region 辅助类
    class LoginClient : ApiClient
    {
        public static Int32 LoginCalledCount;

        protected override Task<Object?> OnLoginAsync(ISocketClient client, Boolean force, CancellationToken cancellationToken = default)
        {
            Interlocked.Increment(ref LoginCalledCount);
            return Task.FromResult<Object?>("LoginResult");
        }
    }
    #endregion
}