v9.10.2019.0101  全面巩固批量Insert/Update/Upsert,支持数据备份、恢复和同步,支持实体列表保存到文件以及加载
大石头 authored at 2019-01-01 13:38:33
7.20 KiB
X
using NewLife;
using NewLife.Data;
using NewLife.Http;
using Xunit;

namespace XUnitTest.Http;

/// <summary>HttpBase 公共逻辑测试</summary>
public class HttpBaseTests
{
    [Theory]
    [InlineData("GET / ")]
    [InlineData("HTTP/1.1 200")]
    [InlineData("POST /api ")]
    public void FastValidHeader_Positive(String first)
    {
        var data = first.GetBytes();
        Assert.True(HttpBase.FastValidHeader(data));
    }

    [Theory(DisplayName = "Content-Length_合法则解析或标记非法")]
    [InlineData("POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello", 5, false)]
    [InlineData("POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", 0, false)]
    [InlineData("POST / HTTP/1.1\r\nHost: a\r\n\r\n", -1, false)]
    [InlineData("POST / HTTP/1.1\r\nContent-Length: -1\r\n\r\n", -1, true)]
    [InlineData("POST / HTTP/1.1\r\nContent-Length: abc\r\n\r\n", -1, true)]
    [InlineData("POST / HTTP/1.1\r\nContent-Length: 9999999999\r\n\r\n", -1, true)]
    // 重复(含大小写不同)的 Content-Length 会与前置代理的取值理解不一致,构成 CL.CL 走私面,同样判非法
    [InlineData("POST / HTTP/1.1\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello", -1, true)]
    [InlineData("POST / HTTP/1.1\r\nContent-Length: 5\r\ncontent-length: 3\r\n\r\nhello", -1, true)]
    public void ContentLength_ParsedOrMarkedInvalid(String raw, Int32 expectLength, Boolean expectInvalid)
    {
        var req = new HttpRequest();
        Assert.True(req.Parse(new ArrayPacket(raw.GetBytes())));

        // 非法值不能退化成“无体”(ContentLength=-1 且未标记),否则请求边界失步
        Assert.Equal(expectLength, req.ContentLength);
        Assert.Equal(expectInvalid, req.InvalidContentLength);
    }

    [Fact(DisplayName = "Transfer-Encoding存在时_主体不按Content-Length截断")]
    public void TransferEncoding_OverridesContentLength()
    {
        // RFC 9112 §6.1:TE 覆盖 CL。按 CL 截断会把同轮其余字节整段丢掉——服务端分块解码拿不到完整流
        // (实测请求悬停),故 TE 存在时主体为头部之后的全部可用字节,消耗量即整包长度
        var raw = "POST /cs HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
        var bytes = raw.GetBytes();

        var req = new HttpRequest();
        Assert.True(req.Parse(new ArrayPacket(bytes)));

        Assert.Equal("5\r\nhello\r\n0\r\n\r\n", req.Body!.ToStr());
        Assert.Equal(bytes.Length, req.ConsumedLength);
    }

    [Fact(DisplayName = "无长度请求_主体为空且消耗只含头部")]
    public void Parse_RequestWithoutLength_HasNoBody()
    {
        // RFC 9112 §6.3:请求既无 CL 也无 TE 时没有实体,头部之后的字节属于同连接的下一个请求(流水线)。
        // 按实体吞掉会让第二个请求静默丢失(不回响应也不关连接)
        var first = "GET /a HTTP/1.1\r\nHost: a\r\n\r\n";
        var raw = first + "GET /b HTTP/1.1\r\nHost: b\r\n\r\n";

        var req = new HttpRequest();
        Assert.True(req.Parse(new ArrayPacket(raw.GetBytes())));

        Assert.Null(req.Body);
        Assert.Equal(-1, req.ContentLength);
        Assert.Equal(first.Length, req.ConsumedLength);
    }

    [Fact(DisplayName = "HTTP1.0的POST无长度_保留读到的都是实体的兼容语义")]
    public void Parse_Http10PostWithoutLength_KeepsBodyCompat()
    {
        // 老式客户端以连接关闭定界实体、不发 Content-Length,兼容语义按方法保留(HTTP/1.0 的 POST/PUT/PATCH)
        var req = new HttpRequest();
        Assert.True(req.Parse(new ArrayPacket("POST /a HTTP/1.0\r\nHost: a\r\n\r\nhello".GetBytes())));

        Assert.Equal("hello", req.Body!.ToStr());

        // HTTP/1.1 的 POST 不再按实体处理(同上,无长度即无实体)
        var req2 = new HttpRequest();
        Assert.True(req2.Parse(new ArrayPacket("POST /a HTTP/1.1\r\nHost: a\r\n\r\nhello".GetBytes())));

        Assert.Null(req2.Body);
    }

    [Fact(DisplayName = "请求同实例二次解析_上一条头部不残留")]
    public void Reparse_SameInstance_ClearsStaleHeaders()
    {
        // 残留判据比缺省值更危险:上一条的 Transfer-Encoding/Content-Length 会让本条按错误的方式分帧
        var req = new HttpRequest();
        var first = "POST /a HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: gzip\r\nContent-Length: 5\r\nX-Old: 1\r\n\r\nhello";
        Assert.True(req.Parse(new ArrayPacket(first.GetBytes())));
        Assert.Equal("gzip", req.Headers["Transfer-Encoding"]);

        Assert.True(req.Parse(new ArrayPacket("GET /b HTTP/1.1\r\nHost: b\r\n\r\n".GetBytes())));

        Assert.Null(req.Headers["Transfer-Encoding"]);
        Assert.Null(req.Headers["Content-Length"]);
        Assert.Null(req.Headers["X-Old"]);
        Assert.Equal("b", req.Headers["Host"]);
        Assert.Equal(-1, req.ContentLength);
    }

    [Fact(DisplayName = "响应同实例二次解析_上一条头部不残留")]
    public void ResponseReparse_SameInstance_ClearsStaleHeaders()
    {
        // 客户端据响应头部分帧(Transfer-Encoding 优先于 Content-Length),残留会让下一条响应被错误解码
        var res = new HttpResponse();
        var first = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\nhello";
        Assert.True(res.Parse(new ArrayPacket(first.GetBytes())));
        Assert.Equal("chunked", res.Headers["Transfer-Encoding"]);

        Assert.True(res.Parse(new ArrayPacket("HTTP/1.1 204 No Content\r\n\r\n".GetBytes())));

        Assert.Null(res.Headers["Transfer-Encoding"]);
        Assert.Null(res.Headers["Content-Length"]);
    }

    [Fact(DisplayName = "FastParse只分析首行_不残留上一条头部")]
    public void FastParse_ClearsStaleHeaders()
    {
        // FastParse 不解析头部,故也不应残留:否则 Host 会沿用上一条请求的值
        var req = new HttpRequest();
        Assert.True(req.Parse(new ArrayPacket("GET /a HTTP/1.1\r\nHost: a\r\n\r\n".GetBytes())));
        Assert.Equal("a", req.Host);

        Assert.True(req.FastParse(new ArrayPacket("GET /b HTTP/1.1\r\n\r\n".GetBytes())));

        Assert.Null(req.Headers["Host"]);
        Assert.Null(req.Host);
    }

    [Fact(DisplayName = "同实例二次解析_上一条主体句柄已归还")]
    public void Reparse_SameInstance_ReleasesStaleBody()
    {
        // 旧主体是上一次解析的拥有句柄(源包共享切片):覆盖而不归还,其池缓冲引用计数永不归零
        var bytes = "POST /a HTTP/1.1\r\nHost: a\r\nContent-Length: 3\r\n\r\nabc".GetBytes();
        using var src = new OwnerPacket(bytes.Length);
        bytes.CopyTo(src.GetSpan());

        var req = new HttpRequest();
        Assert.True(req.Parse(src));

        var body = (OwnerPacket)req.Body!;
        Assert.Equal("abc", body.ToStr());
        Assert.Equal(2, src.RefCount);   // 源句柄 + 主体切片

        Assert.True(req.Parse(src));

        // 旧主体已释放(Dispose 置空 _owner,RefCount 读作 0 哨兵值);源句柄 + 新主体仍为 2,
        // 既证明旧句柄已归还,也证明没有过度释放
        Assert.Equal(0, body.RefCount);
        Assert.Equal(2, src.RefCount);
    }
}