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

namespace XUnitTest.Http;

public class HttpHelperTests
{
    static HttpHelperTests() => HttpHelper.Tracer = new DefaultTracer();

    class MyHandler : HttpMessageHandler
    {
        public Byte[] Data { get; set; }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            //var body = await request.Content.ReadAsByteArrayAsync();

            var rs = new HttpResponseMessage(HttpStatusCode.OK);

            if (Data != null)
                rs.Content = new ByteArrayContent(Data);
            else rs.Content = request.Content != null ? request.Content : new StringContent(request.Headers.ToString());

            return Task.FromResult(rs);
        }
    }

    [Fact]
    public async Task PostJson()
    {
        var url = "http://star.newlifex.com/cube/info";

        var client = new HttpClient(new MyHandler());
        client.SetUserAgent();

        var rs = client.PostJson(url, new { state = "1234", state2 = "abcd" });
        Assert.NotNull(rs);
        Assert.Equal("""{"state":"1234","state2":"abcd"}""", rs);

        rs = await client.PostJsonAsync(url, new { state = "1234", state2 = "abcd" });
        Assert.NotNull(rs);
        Assert.Equal("""{"state":"1234","state2":"abcd"}""", rs);
    }

    [Fact]
    public async Task PostXml()
    {
        var url = "http://star.newlifex.com/cube/info";

        var client = new HttpClient(new MyHandler());
        var rs = client.PostXml(url, new { state = "1234", state2 = "abcd" });
        Assert.NotNull(rs);
        Assert.Contains("""
            <_f__AnonymousType4_2>
              <state>1234</state>
              <state2>abcd</state2>
            </_f__AnonymousType4_2>
            """, rs);

        rs = await client.PostXmlAsync(url, new { state = "1234", state2 = "abcd" });
        Assert.NotNull(rs);
        Assert.Contains("""
            <_f__AnonymousType4_2>
              <state>1234</state>
              <state2>abcd</state2>
            </_f__AnonymousType4_2>
            """, rs);
    }

    [Fact]
    public async Task PostForm()
    {
        var url = "http://star.newlifex.com/cube/info";

        var client = new HttpClient(new MyHandler());
        var rs = client.PostForm(url, new { state = "1234", state2 = "abcd" });
        Assert.NotNull(rs);
        Assert.Contains("state=1234&state2=abcd", rs);

        rs = await client.PostFormAsync(url, new { state = "1234", state2 = "abcd" });
        Assert.NotNull(rs);
        Assert.Contains("state=1234&state2=abcd", rs);
    }

    [Fact]
    public void GetString()
    {
        var url = "http://star.newlifex.com/cube/info";

        var client = new HttpClient(new MyHandler());
        var rs = client.GetString(url, new Dictionary<String, String> { { "state", "xxxyyy" } });

        Assert.NotNull(rs);
        Assert.Equal("state: xxxyyy\r\n", rs);
    }

    [Fact]
    public async Task DownloadFile()
    {
        var url = "http://star.newlifex.com/cube/info";
        var file = "down.txt";
        var file2 = file.GetFullPath();

        if (File.Exists(file2)) File.Delete(file2);

        var txt = "学无先后达者为师!";
        var sb = new StringBuilder();
        for (var i = 0; i < 1000; i++)
        {
            sb.AppendLine(txt);
        }

        var client = new HttpClient(new MyHandler { Data = sb.ToString().GetBytes() });
        await client.DownloadFileAsync(url, file);

        Assert.True(File.Exists(file2));
    }

    [Fact]
    public async Task UploadFile()
    {
        var url = "http://star.newlifex.com/cube/info";
        var file = "up.txt";
        var file2 = file.GetFullPath();

        var txt = "学无先后达者为师!";
        File.WriteAllText(file2, txt);

        var client = new HttpClient(new MyHandler());
        var rs = await client.UploadFileAsync(url, file, new { state = "1234", state2 = "abcd" });

        Assert.NotNull(rs);
        //Assert.Equal("""
        //    --05331b02-8d38-4905-902f-119335443546
        //    Content-Disposition: form-data; name=file; filename=up.txt; filename*=utf-8''up.txt

        //    学无先后达者为师!
        //    --05331b02-8d38-4905-902f-119335443546
        //    Content-Type: text/plain; charset=utf-8
        //    Content-Disposition: form-data; name=state

        //    1234
        //    --05331b02-8d38-4905-902f-119335443546
        //    Content-Type: text/plain; charset=utf-8
        //    Content-Disposition: form-data; name=state2

        //    abcd
        //    --05331b02-8d38-4905-902f-119335443546--

        //    """, rs);
        Assert.Contains("Content-Disposition: form-data; name=file; filename=up.txt; filename*=utf-8''up.txt", rs);
        Assert.Contains(txt, rs);
        Assert.Contains("Content-Type: text/plain; charset=utf-8", rs);
        Assert.Contains("Content-Disposition: form-data; name=state", rs);
        Assert.Contains("1234", rs);
        Assert.Contains("Content-Disposition: form-data; name=state2", rs);
        Assert.Contains("abcd", rs);
    }

    [Fact]
    public void GetStringWithDnsResolver()
    {
        var url = "http://star.newlifex.com/cube/info";

        // 外部网络用例:断网或代理异常时请求会长时间挂起,先做限时可达性探测
        if (!TestNetworkHelper.IsReachable("star.newlifex.com", 80)) return;

        var client = DefaultTracer.Instance.CreateHttpClient();
        client.Timeout = TimeSpan.FromSeconds(10);
        var rs = client.GetString(url, new Dictionary<String, String> { { "state", "xxxyyy" } });

        Assert.NotNull(rs);
        //Assert.Equal("state: xxxyyy\r\n", rs);
        Assert.Contains("\"name\":\"StarWeb\"", rs, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void GetStringWithDnsResolver2()
    {
        var url = "https://sso.newlifex.com/cube/info";

        // 外部网络用例:断网或代理异常时请求会长时间挂起,先做限时可达性探测
        if (!TestNetworkHelper.IsReachable("sso.newlifex.com", 443)) return;

        var client = DefaultTracer.Instance.CreateHttpClient();
        client.Timeout = TimeSpan.FromSeconds(10);
        var rs = client.GetString(url, new Dictionary<String, String> { { "state", "xxxyyy" } });

        Assert.NotNull(rs);
        //Assert.Equal("state: xxxyyy\r\n", rs);
        Assert.Contains("\"name\":\"CubeSSO\"", rs);
    }

    [Fact(DisplayName = "MakeResponse_无实体_补Content-Length为零")]
    public void MakeResponse_NoBody_WritesZeroContentLength()
    {
        // 不声明长度时报文不可自定界,keep-alive 下客户端会把下一个响应当成本次实体
        var pk = HttpHelper.MakeResponse(HttpStatusCode.OK, null, null);
        var text = pk.ToStr();

        Assert.Contains("Content-Length: 0\r\n", text);
        Assert.EndsWith("\r\n\r\n", text);
    }

    [Fact(DisplayName = "MakeResponse_有实体_写明长度并接上负载")]
    public void MakeResponse_WithBody_WritesLength()
    {
        var pk = HttpHelper.MakeResponse(HttpStatusCode.OK, null, new ArrayPacket("hello".GetBytes()));
        var text = pk.ToStr();

        Assert.Contains("Content-Length: 5\r\n", text);
        Assert.EndsWith("hello", text);
    }

    [Theory(DisplayName = "MakeResponse_无实体状态码_不声明Content-Length")]
    [InlineData(HttpStatusCode.Continue)]
    [InlineData(HttpStatusCode.NoContent)]
    [InlineData(HttpStatusCode.NotModified)]
    public void MakeResponse_NoEntityStatus_OmitsContentLength(HttpStatusCode code)
    {
        var pk = HttpHelper.MakeResponse(code, null, null);

        // RFC 7230 §3.3.2:1xx/204/304 不得携带 Content-Length
        Assert.DoesNotContain("Content-Length", pk.ToStr());
    }

    [Fact(DisplayName = "MakeResponse_调用方已声明长度_不重复写入")]
    public void MakeResponse_ExistingContentLength_NotDuplicated()
    {
        // 重复的 Content-Length 是 CL.CL 走私面,宁可少补也不能补出第二个
        var headers = new Dictionary<String, Object?> { ["Content-Length"] = "0" };
        var pk = HttpHelper.MakeResponse(HttpStatusCode.OK, headers, null);
        var text = pk.ToStr();

        Assert.Equal(1, text.Split("Content-Length").Length - 1);
    }

    [Fact(DisplayName = "MakeResponse_已声明分块传输_不补Content-Length")]
    public void MakeResponse_TransferEncodingHeader_NoContentLength()
    {
        var headers = new Dictionary<String, Object?> { ["Transfer-Encoding"] = "chunked" };
        var pk = HttpHelper.MakeResponse(HttpStatusCode.OK, headers, null);
        var text = pk.ToStr();

        Assert.DoesNotContain("Content-Length", text);
        Assert.Contains("Transfer-Encoding: chunked", text);
    }

    #region 资源释放
    /// <summary>带释放标记的响应</summary>
    class TrackingResponse : HttpResponseMessage
    {
        public Boolean Disposed { get; private set; }

        public TrackingResponse(HttpStatusCode code) : base(code) { }

        protected override void Dispose(Boolean disposing)
        {
            Disposed = true;
            base.Dispose(disposing);
        }
    }

    /// <summary>带释放标记的流</summary>
    class TrackingStream : MemoryStream
    {
        public Boolean Disposed { get; private set; }

        protected override void Dispose(Boolean disposing)
        {
            Disposed = true;
            base.Dispose(disposing);
        }
    }

    /// <summary>把内部流直接作为读流的响应内容</summary>
    class TrackingStreamContent : HttpContent
    {
        private readonly Stream _stream;

        public TrackingStreamContent(Stream stream) => _stream = stream;

        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => _stream.CopyToAsync(stream);

        protected override Boolean TryComputeLength(out Int64 length)
        {
            length = _stream.Length;
            return true;
        }

        protected override Task<Stream> CreateContentReadStreamAsync() => Task.FromResult(_stream);
    }

    /// <summary>按工厂返回响应的处理器</summary>
    class ResponseHandler : HttpMessageHandler
    {
        private readonly Func<HttpResponseMessage> _factory;

        public ResponseHandler(Func<HttpResponseMessage> factory) => _factory = factory;

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(_factory());
    }

    [Fact(DisplayName = "HttpHelper_Post_响应被释放")]
    public async Task PostAsync_DisposesResponse()
    {
        var response = new TrackingResponse(HttpStatusCode.OK) { Content = new StringContent("ok") };
        var client = new HttpClient(new ResponseHandler(() => response));

        var rs = await client.PostJsonAsync("http://x/y", new { a = 1 });

        // 响应为本次调用独占,读取后立即释放(旧实现靠 GC,标记不会置位)
        Assert.Equal("ok", rs);
        Assert.True(response.Disposed);
    }

    [Fact(DisplayName = "HttpHelper_下载_响应流被释放")]
    public async Task DownloadFile_DisposesResponseStream()
    {
        var stream = new TrackingStream();
        stream.Write(Encoding.UTF8.GetBytes("hello"));
        stream.Position = 0;

        var content = new TrackingStreamContent(stream);
        var client = new HttpClient(new ResponseHandler(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = content }));

        var file = Path.GetTempFileName();
        try
        {
            await client.DownloadFileAsync("http://x/y", file);

            // 下载完成即释放响应流,不等 GC
            Assert.True(stream.Disposed);
            Assert.Equal("hello", File.ReadAllText(file));
        }
        finally
        {
            if (File.Exists(file)) File.Delete(file);
        }
    }

    [Fact(DisplayName = "HttpHelper_上传文件_文件句柄及时释放")]
    public async Task UploadFile_ReleasesFileHandle()
    {
        var file = Path.GetTempFileName();
        await File.WriteAllTextAsync(file, "hello");
        try
        {
            var client = new HttpClient(new MyHandler());
            var rs = await client.UploadFileAsync("http://x/up", file);
            Assert.NotNull(rs);

            // 上传完成后文件句柄必须已关闭:旧实现挂在未释放的 multipart 上等 GC,Windows 下持续占用文件
            if (Runtime.Windows)
            {
                using var fs = new FileStream(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                Assert.True(fs.CanWrite);
            }
        }
        finally
        {
            File.Delete(file);
        }
    }
    #endregion
}