解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
12.65 KiB
X
using System.Buffers;
using System.ComponentModel;
using System.Text;
using NewLife;
using NewLife.Data;
using NewLife.Http;
using NewLife.Messaging;
using Xunit;

namespace XUnitTest.Messaging;

/// <summary>WebSocket 消息编解码器(WebSocketCodec)测试</summary>
public class WebSocketCodecTests
{
    #region 工具
    private static readonly WebSocketCodec _serverCodec = new() { IsServer = true };
    private static readonly WebSocketCodec _clientCodec = new() { IsServer = false };

    private static Byte[] MakePayload(Int32 count, Byte seed = 0x5A)
    {
        var buf = new Byte[count];
        for (var i = 0; i < count; i++) buf[i] = (Byte)(i * 13 + seed);

        return buf;
    }
    #endregion

    #region 解析
    [Fact]
    [DisplayName("WS编解码_帧长度非法_标记损坏而非等待更多数据")]
    public void TryParse_NegativeLength_MarksInvalid()
    {
        // 0x81 = FIN + 文本帧;0xFF = 掩码位 + 长度127,随后 8 字节大端长度最高位为 1 ⇒ 负数
        var frame = new Byte[] { 0x81, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

        var rs = _serverCodec.TryParse(new ReadOnlySequence<Byte>(frame));

        // 必须标记损坏:当作“数据不足”会让连接永久等待一个永远不会完整的帧
        Assert.NotNull(rs);
        Assert.True(rs!.Value.Invalid);
    }

    [Fact]
    [DisplayName("WS编解码_头部未到齐_返回空等待更多数据")]
    public void TryParse_IncompleteHeader_ReturnsNull()
    {
        // 0xFE = 掩码位 + 长度126,还需 2 字节扩展长度
        var frame = new Byte[] { 0x81, 0xFE };

        var rs = _serverCodec.TryParse(new ReadOnlySequence<Byte>(frame));

        Assert.Null(rs);
    }

    [Fact]
    [DisplayName("WS编解码_文本帧无掩码_定界")]
    public void TryParse_TextFrame()
    {
        var frame = new Byte[] { 0x81, 0x05, (Byte)'h', (Byte)'e', (Byte)'l', (Byte)'l', (Byte)'o' };

        var rs = _serverCodec.TryParse(new ArrayPacket(frame).AsReadOnlySequence());
        Assert.NotNull(rs);
        var ws = Assert.IsType<WsMessage>(rs.Value.Message);
        Assert.True(ws.Fin);
        Assert.Equal(WebSocketMessageType.Text, ws.Type);
        Assert.Null(ws.MaskKey);
        Assert.Equal(2, rs.Value.HeaderSize);
        Assert.Equal(5L, rs.Value.BodyLength);

        // 头不足:等待(不产生对象)
        Assert.Null(_serverCodec.TryParse(new ArrayPacket(new Byte[] { 0x81 }).AsReadOnlySequence()));
    }

    [Fact]
    [DisplayName("WS编解码_掩码帧_密钥随头部消费")]
    public void TryParse_MaskedFrame()
    {
        // 客户端帧:FIN+Text + MASK+len5 + key(4) + 掩码负载
        var key = new Byte[] { 0x11, 0x22, 0x33, 0x44 };
        var payload = Encoding.UTF8.GetBytes("hello");
        var masked = new Byte[payload.Length];
        for (var i = 0; i < payload.Length; i++) masked[i] = (Byte)(payload[i] ^ key[i % 4]);

        var frame = new Byte[] { 0x81, 0x85, key[0], key[1], key[2], key[3], masked[0], masked[1], masked[2], masked[3], masked[4] };

        var rs = _serverCodec.TryParse(new ArrayPacket(frame).AsReadOnlySequence());
        Assert.NotNull(rs);
        var ws = Assert.IsType<WsMessage>(rs.Value.Message);
        Assert.Equal(key, ws.MaskKey);
        Assert.Equal(6, rs.Value.HeaderSize);
        Assert.Equal(5L, rs.Value.BodyLength);
    }

    [Fact]
    [DisplayName("WS编解码_扩展长度_2字节与8字节")]
    public void TryParse_ExtendedLength()
    {
        // 126 → 2 字节大端(200)
        var f2 = new Byte[] { 0x82, 0x7E, 0x00, 0xC8 };
        var rs2 = _serverCodec.TryParse(new ArrayPacket(f2).AsReadOnlySequence());
        Assert.NotNull(rs2);
        Assert.Equal(4, rs2.Value.HeaderSize);
        Assert.Equal(200L, rs2.Value.BodyLength);

        // 127 → 8 字节大端(70000)
        var f8 = new Byte[] { 0x82, 0x7F, 0, 0, 0, 0, 0, 0x01, 0x11, 0x70 };
        var rs8 = _serverCodec.TryParse(new ArrayPacket(f8).AsReadOnlySequence());
        Assert.NotNull(rs8);
        Assert.Equal(10, rs8.Value.HeaderSize);
        Assert.Equal(70000L, rs8.Value.BodyLength);
    }

    [Fact]
    [DisplayName("WS编解码_非法帧_返回失败")]
    public void TryParse_Invalid()
    {
        // 分片帧(FIN=0):照常解析(Fin=false),重组由消费侧(WebSocket/WebSocketClient)完成
        var frag = _serverCodec.TryParse(new ArrayPacket(new Byte[] { 0x01, 0x03, 0x61 }).AsReadOnlySequence());
        Assert.NotNull(frag);
        var fragMsg = (WsMessage)frag.Value.Message!;
        Assert.False(fragMsg.Fin);
        Assert.Equal(WebSocketMessageType.Text, fragMsg.Type);
        fragMsg.Dispose();

        // 8 字节长度最高位为 1(负数):损坏帧,须标记 Invalid 而不是当作"数据不足"继续等
        var neg = new Byte[] { 0x82, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
        var negRs = _serverCodec.TryParse(new ArrayPacket(neg).AsReadOnlySequence());
        Assert.NotNull(negRs);
        Assert.True(negRs!.Value.Invalid);

        // 掩码位已置但密钥不齐:等待
        Assert.Null(_serverCodec.TryParse(new ArrayPacket(new Byte[] { 0x81, 0x85, 0x11, 0x22 }).AsReadOnlySequence()));
    }

    [Fact]
    [DisplayName("WS编解码_RSV位非零或保留opcode_标记损坏")]
    public void TryParse_ReservedBitsOrOpcode_MarksInvalid()
    {
        // RSV1 置位(0xC1 = FIN + RSV1 + opcode=1):未协商扩展时必须为 0
        var rsv = _serverCodec.TryParse(new ArrayPacket(new Byte[] { 0xC1, 0x00 }).AsReadOnlySequence());
        Assert.NotNull(rsv);
        Assert.True(rsv!.Value.Invalid);

        // 保留 opcode 3(0x83 = FIN + opcode=3)
        var op = _serverCodec.TryParse(new ArrayPacket(new Byte[] { 0x83, 0x00 }).AsReadOnlySequence());
        Assert.NotNull(op);
        Assert.True(op!.Value.Invalid);

        // 合法帧不受影响:空文本帧(0x81 + 长度 0)
        var ok = _serverCodec.TryParse(new ArrayPacket(new Byte[] { 0x81, 0x00 }).AsReadOnlySequence());
        Assert.NotNull(ok);
        Assert.False(ok!.Value.Invalid);
        ok.Value.Message?.Dispose();
    }
    #endregion

    #region 构建
    [Fact]
    [DisplayName("WS编解码_服务端构建_无掩码字节与解析一致")]
    public async Task Build_Server()
    {
        var msg = new WsMessage { Type = WebSocketMessageType.Text };
        msg.SetBody(new ArrayPacket(Encoding.UTF8.GetBytes("hello")));

        var pk = _serverCodec.Build(msg);
        Assert.NotNull(pk);
        Assert.NotNull(msg.Payload);     // 构建不消费消息负载

        // 字节:[0x81, 0x05][hello]
        Assert.Equal(new Byte[] { 0x81, 0x05, (Byte)'h', (Byte)'e', (Byte)'l', (Byte)'l', (Byte)'o' }, pk!.AsReadOnlySequence().ToArray());

        // 帧泵端到端:绑定体并读满
        var pump = new MessagePump(_serverCodec);
        using var pipe = new Pipe();
        pipe.Writer.Append(pk);

        Assert.True(pump.TryRead(pipe.Reader, out var recv));
        Assert.Equal("hello", Encoding.UTF8.GetString(recv!.Payload!.ToArray()));
        recv.Dispose();
    }

    [Fact]
    [DisplayName("WS编解码_客户端构建_自动掩码可解码")]
    public void Build_Client_AutoMask()
    {
        var payload = MakePayload(37);
        var msg = new WsMessage { Type = WebSocketMessageType.Binary };
        msg.SetBody(new ArrayPacket(payload));

        var pk = _clientCodec.Build(msg)!;
        var data = pk.AsReadOnlySequence().ToArray();

        // 掩码位置 1(长度字节)
        Assert.Equal(0x82, data[0]);
        Assert.True((data[1] & 0x80) != 0);

        // 服务端解析:掩码键挂在消息上,消费方解码还原
        var pump = new MessagePump(_serverCodec);
        using var pipe = new Pipe();
        pipe.Writer.Append(pk);

        Assert.True(pump.TryRead(pipe.Reader, out var bound));
        var ws = (WsMessage)bound!;
        Assert.NotNull(ws.MaskKey);
        Assert.True(ws.Demask());
        Assert.Equal(payload, ws.Payload!.ToArray());

        // 掩码为独占拷贝:消息负载未被原位改写
        Assert.Equal(payload, msg.Payload!.ToArray());

        ws.Dispose();
        pk.TryDispose();
    }

    [Fact]
    [DisplayName("WS编解码_客户端指定密钥_构建确定性且可解码")]
    public void Build_Client_FixedMask_Deterministic()
    {
        var key = new Byte[] { 0x0A, 0x0B, 0x0C, 0x0D };

        // 同密钥两次构建:字节一致(确定性)
        var nw = new WsMessage { Type = WebSocketMessageType.Binary, MaskKey = key };
        nw.SetBody(new ArrayPacket(MakePayload(64)));
        var frame1 = _clientCodec.Build(nw)!;

        var nw2 = new WsMessage { Type = WebSocketMessageType.Binary, MaskKey = key };
        nw2.SetBody(new ArrayPacket(MakePayload(64)));
        var frame2 = _clientCodec.Build(nw2)!;

        Assert.Equal(frame2.AsReadOnlySequence().ToArray(), frame1.AsReadOnlySequence().ToArray());

        // 服务端解码还原
        var pump = new MessagePump(_serverCodec);
        using var pipe = new Pipe();
        pipe.Writer.Append(frame1);
        Assert.True(pump.TryRead(pipe.Reader, out var bound));
        var ws = (WsMessage)bound!;
        Assert.Equal(key, ws.MaskKey);
        Assert.True(ws.Demask());
        Assert.Equal(MakePayload(64), ws.Payload!.ToArray());
        ws.Dispose();

        frame1.TryDispose();
        frame2.TryDispose();
    }

    [Fact]
    [DisplayName("WS编解码_服务端构建_全边界长度往返")]
    public void Build_Server_RoundTripAllSizes()
    {
        foreach (var count in new[] { 0, 5, 125, 126, 65535, 65536 })
        {
            var payload = MakePayload(count);

            var nw = new WsMessage { Type = WebSocketMessageType.Text };
            nw.SetBody(new ArrayPacket(payload));
            var frame = _serverCodec.Build(nw)!;

            // 客户端解析回读:服务端帧无掩码
            var pump = new MessagePump(_clientCodec);
            using var pipe = new Pipe();
            pipe.Writer.Append(frame);
            Assert.True(pump.TryRead(pipe.Reader, out var bound));
            var ws = (WsMessage)bound!;
            Assert.Equal(payload, ws.Payload!.ToArray());
            ws.Dispose();

            frame.TryDispose();
        }
    }

    [Fact]
    [DisplayName("WS编解码_头部构建_声明长度")]
    public void BuildHeader_DeclaresLength()
    {
        var pk = _serverCodec.BuildHeader(new WsMessage { Type = WebSocketMessageType.Binary }, 300);
        Assert.Equal(new Byte[] { 0x82, 0x7E, 0x01, 0x2C }, pk.AsReadOnlySequence().ToArray());
        pk.TryDispose();

        // 客户端方向:带掩码帧不支持流式发送
        Assert.Throws<NotSupportedException>(() => _clientCodec.BuildHeader(new WsMessage(), 10));

        // 非法长度
        Assert.Throws<ArgumentOutOfRangeException>(() => _serverCodec.BuildHeader(new WsMessage(), -1));
    }

    [Fact]
    [DisplayName("WS编解码_关闭帧_状态码与描述解析")]
    public void CloseStatus_Parse()
    {
        var desc = "bye";
        var descBytes = Encoding.UTF8.GetBytes(desc);
        var body = new Byte[2 + descBytes.Length];
        body[0] = 0x03;
        body[1] = 0xE8;     // 1000 网络序
        descBytes.CopyTo(body, 2);

        var msg = new WsMessage { Type = WebSocketMessageType.Close };
        msg.SetBody(new ArrayPacket(body));

        Assert.True(msg.TryReadCloseStatus());
        Assert.Equal(1000, msg.CloseStatus);
        Assert.Equal(desc, msg.StatusDescription);

        msg.Dispose();
    }
    #endregion

    [Fact]
    [DisplayName("WS编解码_接收掩码帧_消息解码还原")]
    public void Receive_MaskedFrame_Demask()
    {
        var expected = MakePayload(20);
        var msg = new WsMessage { Type = WebSocketMessageType.Binary };
        msg.SetBody(new ArrayPacket(MakePayload(20)));
        var frame = _clientCodec.Build(msg)!;

        // 服务端解析:掩码键挂在消息上
        var rs = _serverCodec.TryParse(frame.AsReadOnlySequence());
        Assert.NotNull(rs);
        Assert.NotNull(((WsMessage)rs.Value.Message!).MaskKey);

        // 帧泵绑体(内存视图)后解码还原
        var pump = new MessagePump(_serverCodec);
        using var pipe = new Pipe();
        pipe.Writer.Append(frame);

        Assert.True(pump.TryRead(pipe.Reader, out var bound));
        var ws = (WsMessage)bound!;
        Assert.True(ws.Demask());
        Assert.Equal(expected, ws.Payload!.ToArray());
        ws.Dispose();

        // 无掩码消息:解码为无操作
        var plain = new WsMessage { Type = WebSocketMessageType.Text };
        plain.SetBody(new ArrayPacket(expected));
        Assert.False(plain.Demask());
        plain.Dispose();
    }

    #region 辅助
    #endregion
}