v9.10.2019.0101  全面巩固批量Insert/Update/Upsert,支持数据备份、恢复和同步,支持实体列表保存到文件以及加载
大石头 authored at 2019-01-01 13:38:33
20.54 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_IncompleteHeader_NoAllocation()
    {
        // 1 字节:帧头至少 2 字节
        var one = new ReadOnlySequence<Byte>(new Byte[] { 0x81 });
        Assert.Null(_serverCodec.TryParse(one));

        var memory = GC.GetAllocatedBytesForCurrentThread();
        var rs = _serverCodec.TryParse(one);
        var used = GC.GetAllocatedBytesForCurrentThread() - memory;

        Assert.Null(rs);
        Assert.True(used == 0, $"帧头未到齐时不应产生对象,本次分配 {used} 字节");

        // 掩码帧:长度字符与掩码键均未到齐
        var masked = new ReadOnlySequence<Byte>(new Byte[] { 0x81, 0x85, 0x11, 0x22 });
        Assert.Null(_serverCodec.TryParse(masked));

        var memory2 = GC.GetAllocatedBytesForCurrentThread();
        var rs2 = _serverCodec.TryParse(masked);
        var used2 = GC.GetAllocatedBytesForCurrentThread() - memory2;

        Assert.Null(rs2);
        Assert.True(used2 == 0, $"掩码键未到齐时不应产生对象,本次分配 {used2} 字节");
    }

    [Fact]
    [DisplayName("WS消息_实例解析_字段就位且失败路径无副作用")]
    public void TryParse_Instance_FillsFields()
    {
        // 掩码文本帧头:FIN+Text + MASK+len5 + key
        var frame = new ReadOnlySequence<Byte>(new Byte[] { 0x81, 0x85, 0x11, 0x22, 0x33, 0x44 });
        var msg = new WsMessage();
        Assert.True(msg.TryParse(frame, out var bodyLength, out var headerSize, out var invalid));
        Assert.False(invalid);
        Assert.Equal(6, headerSize);
        Assert.Equal(5L, bodyLength);
        Assert.True(msg.Fin);
        Assert.Equal(WebSocketMessageType.Text, msg.Type);
        Assert.Equal(new Byte[] { 0x11, 0x22, 0x33, 0x44 }, msg.MaskKey);

        // 扩展长度未到齐:不写入实例
        var keep = new WsMessage { Fin = true, Type = WebSocketMessageType.Ping, MaskKey = new Byte[] { 1, 2, 3, 4 } };
        Assert.False(keep.TryParse(new ReadOnlySequence<Byte>(new Byte[] { 0x82, 0x7E }), out _, out _, out var invalid1));
        Assert.False(invalid1);
        Assert.Equal(WebSocketMessageType.Ping, keep.Type);
        Assert.Equal(new Byte[] { 1, 2, 3, 4 }, keep.MaskKey);

        // 保留 opcode:标记损坏,同样不写入实例
        Assert.False(keep.TryParse(new ReadOnlySequence<Byte>(new Byte[] { 0x83, 0x00 }), out _, out _, out var invalid2));
        Assert.True(invalid2);
        Assert.Equal(WebSocketMessageType.Ping, keep.Type);
    }

    [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编解码_掩码键超过4字节_只写前4字节")]
    public void Build_MaskKeyLongerThanFour_WritesFourBytes()
    {
        // 掩码键在线上固定 4 字节(RFC 6455 §5.2)。若按数组实际长度写头,帧头会多出 (长度-4) 字节,
        // 对端把多出的字节当成负载开头,整条消息错位
        var payload = MakePayload(10);
        var msg = new WsMessage { Type = WebSocketMessageType.Binary };
        var key = new Byte[8];
        for (var i = 0; i < key.Length; i++) key[i] = (Byte)(0x10 + i);
        msg.MaskKey = key;
        msg.SetBody(new ArrayPacket(payload));

        var frame = _clientCodec.Build(msg)!;
        var bytes = frame.AsReadOnlySequence().ToArray();

        // 头 2 + 掩码 4 = 6,加载荷 10 = 16;多余的掩码字节不得进入帧
        Assert.Equal(16, bytes.Length);

        var rs = _serverCodec.TryParse(new ReadOnlySequence<Byte>(bytes));
        Assert.NotNull(rs);
        Assert.Equal(6, rs!.Value.HeaderSize);
        Assert.Equal(10L, rs.Value.BodyLength);

        // 掩码字段必须是键的前 4 字节;且解码后负载与原文一致——
        // 按 8 字节写头会把键的后 4 字节覆盖到负载前 4 字节上
        Assert.Equal(key.AsSpan(0, 4).ToArray(), bytes[2..6]);
        var parsed = (WsMessage)rs.Value.Message!;
        parsed.SetBody(new ArrayPacket(bytes).Slice(6, 10));
        parsed.Demask();
        Assert.Equal(payload, parsed.Payload!.ToArray());
        parsed.Dispose();

        msg.Dispose();
        frame.TryDispose();
    }

    [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();
    }

    [Fact]
    [DisplayName("WS编解码_关闭帧原因超长_截断到控制帧上限")]
    public void ClosePayload_LongReason_Truncated()
    {
        // RFC 6455 §5.5:控制帧负载不得超过 125 字节,即状态码 2 字节 + 原因最多 123 字节
        var pk = WebSocketCodec.BuildClosePayload(1000, new String('x', 300));
        Assert.Equal(125L, pk.Total);
        Assert.Equal(1000, (pk[0] << 8) | pk[1]);
        pk.TryDispose();

        // 多字节字符不得被截成半个:'中' 占 3 字节,123 恰是边界(3×41),整 123 字节保留
        var pk2 = WebSocketCodec.BuildClosePayload(1000, new String('中', 100));
        Assert.Equal(125L, pk2.Total);
        pk2.TryDispose();

        // 加 2 字节 ASCII 前缀后 123 落在 '中' 的续字节上,须退到该字符首字节(122)
        var pk3 = WebSocketCodec.BuildClosePayload(1000, "xy" + new String('中', 100));
        Assert.Equal(124L, pk3.Total);
        pk3.TryDispose();
    }

    [Fact]
    [DisplayName("WS编解码_不可发送的关闭码_退化为无状态码关闭帧")]
    public void ClosePayload_UnsendableStatus_Dropped()
    {
        // 保留值与未分配段禁止端点发送,发出会被对端判定协议错误
        foreach (var code in new[] { 0, 999, 1004, 1005, 1006, 1015, 1016, 2999, 5000 })
        {
            var pk = WebSocketCodec.BuildClosePayload(code, "bye");
            Assert.Equal(0L, pk.Total);
            pk.TryDispose();
        }

        // 已分配段照常携带状态码(无原因时正文仅 2 字节)
        foreach (var code in new[] { 1000, 1003, 1007, 1011, 1014, 3000, 4999 })
        {
            var pk = WebSocketCodec.BuildClosePayload(code, null);
            Assert.Equal(2L, pk.Total);
            Assert.Equal(code, (pk[0] << 8) | pk[1]);
            pk.TryDispose();
        }
    }
    #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 UTF-8 校验(RFC 6455 §8.1)
    [Fact]
    [DisplayName("WS编解码_UTF8校验_合法序列通过")]
    public void IsValidUtf8_ValidPasses()
    {
        Assert.True(WebSocketCodec.IsValidUtf8(null));
        Assert.True(WebSocketCodec.IsValidUtf8((ArrayPacket)"hello".GetBytes()));
        Assert.True(WebSocketCodec.IsValidUtf8((ArrayPacket)"中文测试".GetBytes()));
        // 4 字节序列(emoji)与码点边界
        Assert.True(WebSocketCodec.IsValidUtf8((ArrayPacket)"😀".GetBytes()));
        Assert.True(WebSocketCodec.IsValidUtf8((ArrayPacket)new Byte[] { 0xF4, 0x8F, 0xBF, 0xBF })); // U+10FFFF
    }

    [Fact]
    [DisplayName("WS编解码_UTF8校验_链式负载跨段连续")]
    public void IsValidUtf8_ChainSpansSegments()
    {
        // 一个 3 字节序列被切成两个段:跨段仍应判定合法(按段分别校验会把合法数据判成非法)
        var chain = new ArrayPacket(new Byte[] { 0xE4, 0xB8 }) { Next = new ArrayPacket(new Byte[] { 0xAD }) };
        Assert.True(WebSocketCodec.IsValidUtf8(chain));

        // 跨段截断的多字节序列:非法
        var broken = new ArrayPacket(new Byte[] { 0xE4, 0xB8 }) { Next = new ArrayPacket(new Byte[] { 0x41 }) };
        Assert.False(WebSocketCodec.IsValidUtf8(broken));
    }

    [Theory]
    [DisplayName("WS编解码_UTF8校验_非法序列被拒")]
    // 孤立续字节、过长编码(overlong)、代理区、超过 U+10FFFF、非法前导、截断序列
    [InlineData(new Byte[] { 0x80 })]
    [InlineData(new Byte[] { 0xC0, 0x80 })]
    [InlineData(new Byte[] { 0xE0, 0x80, 0x80 })]
    [InlineData(new Byte[] { 0xED, 0xA0, 0x80 })]
    [InlineData(new Byte[] { 0xF5, 0x80, 0x80, 0x80 })]
    [InlineData(new Byte[] { 0xFF })]
    [InlineData(new Byte[] { 0xE4, 0xB8 })]
    [InlineData(new Byte[] { 0xC3, 0x28 })]
    public void IsValidUtf8_InvalidRejected(Byte[] data) => Assert.False(WebSocketCodec.IsValidUtf8((ArrayPacket)data));
    #endregion

    #region 辅助
    #endregion
}