From 59588f9d7f783c7f6dd86d15125aad59906f4488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=9E=E6=BE=8B?= Date: Tue, 28 May 2024 15:01:18 +0800 Subject: [PATCH] fix: support get and set of exception fields --- .github/workflows/test.yml | 2 +- Tea/TeaCore.cs | 16 +- Tea/TeaException.cs | 73 +--- Tea/TeaResponse.cs | 5 +- Tea/TeaStream.cs | 242 ++++++++++++ Tea/Utils/ReadJsonUtil.cs | 80 ++++ Tea/tea.csproj | 12 +- TeaUnitTests/TeaStreamTest.cs | 514 +++++++++++++++++++++++++ TeaUnitTests/TeaUnitTests.csproj | 2 +- TeaUnitTests/Utils/ReadJsonUtilTest.cs | 30 ++ 10 files changed, 909 insertions(+), 67 deletions(-) create mode 100644 Tea/TeaStream.cs create mode 100644 Tea/Utils/ReadJsonUtil.cs create mode 100644 TeaUnitTests/TeaStreamTest.cs create mode 100644 TeaUnitTests/Utils/ReadJsonUtilTest.cs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8f0e4d..203266a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 2.1.811 + dotnet-version: 3.1.426 - name: install altcover run: dotnet tool install --global altcover.visualizer --version 8.6.14 - name: Install dependencies diff --git a/Tea/TeaCore.cs b/Tea/TeaCore.cs index 45b3c71..71ace50 100644 --- a/Tea/TeaCore.cs +++ b/Tea/TeaCore.cs @@ -77,11 +77,11 @@ public static TeaResponse DoAction(TeaRequest request, Dictionary DoActionAsync(TeaRequest request, Dictiona HttpResponseMessage response = await httpClient.SendAsync(req, new CancellationTokenSource(timeout).Token); return new TeaResponse(response); } - catch (System.Threading.Tasks.TaskCanceledException) + catch (TaskCanceledException) { throw new WebException("operation is timeout"); } @@ -113,7 +113,7 @@ public static async Task DoActionAsync(TeaRequest request, Dictiona public static string GetResponseBody(TeaResponse response) { - using(var ms = new MemoryStream()) + using (var ms = new MemoryStream()) { var buffer = new byte[bufferLength]; var stream = response.Body; @@ -163,16 +163,16 @@ public static Dictionary ConvertHeaders(HttpResponseHeaders head public static bool AllowRetry(IDictionary dict, int retryTimes, long now) { - if(retryTimes == 0) + if (retryTimes == 0) { return true; } - if(!dict.Get("retryable").ToSafeBool(false)) + if (!dict.Get("retryable").ToSafeBool(false)) { return false; } - + int retry; if (dict == null) { @@ -260,7 +260,7 @@ internal static string PercentEncode(string value) } else { - stringBuilder.Append("%").Append(string.Format(CultureInfo.InvariantCulture, "{0:X2}", (int) c)); + stringBuilder.Append("%").Append(string.Format(CultureInfo.InvariantCulture, "{0:X2}", (int)c)); } } diff --git a/Tea/TeaException.cs b/Tea/TeaException.cs index 555999b..38c5e42 100644 --- a/Tea/TeaException.cs +++ b/Tea/TeaException.cs @@ -10,75 +10,38 @@ namespace Tea { public class TeaException : Exception { - private string code; - private string message; - private Dictionary data; - private int statusCode; - private string description; - private Dictionary accessDeniedDetail; - - public string Code - { - get - { - return code; - } - } - - public override string Message - { - get - { - return message; - } - } + public string Code { get; set; } + public new string Message { get; set; } + public new Dictionary Data { get; set; } + public int StatusCode { get; set; } + public string Description { get; set; } + public Dictionary AccessDeniedDetail { get; set; } public Dictionary DataResult { get { - return data; - } - } - - public int StatusCode - { - get - { - return statusCode; - } - } - - - public string Description - { - get - { - return description; + return Data; } } - public Dictionary AccessDeniedDetail + public TeaException() { - get - { - return accessDeniedDetail; - } } public TeaException(IDictionary dict) { Dictionary dicObj = dict.Keys.Cast().ToDictionary(key => key, key => dict[key]); - code = DictUtils.GetDicValue(dicObj, "code").ToSafeString(); - message = DictUtils.GetDicValue(dicObj, "message").ToSafeString(); - description = DictUtils.GetDicValue(dicObj, "description").ToSafeString(); + Code = DictUtils.GetDicValue(dicObj, "code").ToSafeString(); + Message = DictUtils.GetDicValue(dicObj, "message").ToSafeString(); + Description = DictUtils.GetDicValue(dicObj, "description").ToSafeString(); object obj = DictUtils.GetDicValue(dicObj, "accessDeniedDetail"); if (obj != null) { if (typeof(IDictionary).IsAssignableFrom(obj.GetType())) { - IDictionary dicDetail = (IDictionary) obj; - accessDeniedDetail = dicDetail.Keys.Cast().ToDictionary(key => key, key => dicDetail[key]); + IDictionary dicDetail = (IDictionary)obj; + AccessDeniedDetail = dicDetail.Keys.Cast().ToDictionary(key => key, key => dicDetail[key]); } } obj = DictUtils.GetDicValue(dicObj, "data"); @@ -88,11 +51,11 @@ public TeaException(IDictionary dict) } if (typeof(IDictionary).IsAssignableFrom(obj.GetType())) { - IDictionary dicData = (IDictionary) obj; - data = dicData.Keys.Cast().ToDictionary(key => key, key => dicData[key]); - if (DictUtils.GetDicValue(data, "statusCode") != null) + IDictionary dicData = (IDictionary)obj; + Data = dicData.Keys.Cast().ToDictionary(key => key, key => dicData[key]); + if (DictUtils.GetDicValue(Data, "statusCode") != null) { - statusCode = int.Parse(DictUtils.GetDicValue(data, "statusCode").ToSafeString()); + StatusCode = int.Parse(DictUtils.GetDicValue(Data, "statusCode").ToSafeString()); } return; } @@ -105,7 +68,7 @@ public TeaException(IDictionary dict) PropertyInfo p = properties[i]; filedsDict.Add(p.Name, p.GetValue(obj)); } - data = filedsDict; + Data = filedsDict; } } } diff --git a/Tea/TeaResponse.cs b/Tea/TeaResponse.cs index 0d725a4..d618e01 100644 --- a/Tea/TeaResponse.cs +++ b/Tea/TeaResponse.cs @@ -14,6 +14,8 @@ public class TeaResponse public Dictionary Headers { get; set; } + private Stream _body; + public Stream Body { get @@ -29,11 +31,12 @@ public Stream Body } } + public TeaResponse(HttpResponseMessage response) { if (response != null) { - StatusCode = (int) response.StatusCode; + StatusCode = (int)response.StatusCode; StatusMessage = ""; Headers = TeaCore.ConvertHeaders(response.Headers); _responseAsync = response; diff --git a/Tea/TeaStream.cs b/Tea/TeaStream.cs new file mode 100644 index 0000000..91fca7b --- /dev/null +++ b/Tea/TeaStream.cs @@ -0,0 +1,242 @@ +using System.IO; +using System.Text; +using System.Collections.Generic; +using Newtonsoft.Json; +using System.Threading.Tasks; + +using Tea.Utils; +using System; + +namespace Tea +{ + public class TeaStream + { + private const string DATA_PREFIX = "data:"; + private const string EVENT_PREFIX = "event:"; + private const string ID_PREFIX = "id:"; + private const string RETRY_PREFIX = "retry:"; + + public static string ToString(byte[] val) + { + return Encoding.UTF8.GetString(val); + } + + public static object ParseJSON(string val) + { + return JsonConvert.DeserializeObject(val); + } + + public static byte[] ReadAsBytes(Stream stream) + { + int bufferLength = 4096; + using (var ms = new MemoryStream()) + { + var buffer = new byte[bufferLength]; + + while (true) + { + var length = stream.Read(buffer, 0, bufferLength); + if (length == 0) + { + break; + } + + ms.Write(buffer, 0, length); + } + + ms.Seek(0, SeekOrigin.Begin); + var bytes = new byte[ms.Length]; + ms.Read(bytes, 0, bytes.Length); + + stream.Close(); + stream.Dispose(); + + return bytes; + } + } + + public async static Task ReadAsBytesAsync(Stream stream) + { + int bufferLength = 4096; + using (var ms = new MemoryStream()) + { + var buffer = new byte[bufferLength]; + + while (true) + { + var length = await stream.ReadAsync(buffer, 0, bufferLength); + if (length == 0) + { + break; + } + + await ms.WriteAsync(buffer, 0, length); + } + + ms.Seek(0, SeekOrigin.Begin); + var bytes = new byte[ms.Length]; + await ms.ReadAsync(bytes, 0, bytes.Length); + + stream.Close(); + stream.Dispose(); + + return bytes; + } + } + + public static string ReadAsString(Stream stream) + { + return ToString(ReadAsBytes(stream)); + } + + public static async Task ReadAsStringAsync(Stream stream) + { + return ToString(await ReadAsBytesAsync(stream)); + } + + public static object ReadAsJSON(Stream stream) + { + object jResult = ParseJSON(ReadAsString(stream)); + object result = ReadJsonUtil.Deserialize(jResult); + return result; + } + + public async static Task ReadAsJSONAsync(Stream stream) + { + object jResult = ParseJSON(await ReadAsStringAsync(stream)); + object result = ReadJsonUtil.Deserialize(jResult); + return result; + } + + public class SSEEvent + { + public string Data { get; set; } + public string Id { get; set; } + public string Event { get; set; } + public int? Retry { get; set; } + } + + public class EventResult + { + public List Events { get; set; } + public string Remain { get; set; } + + public EventResult(List events, string remain) + { + Events = events; + Remain = remain; + } + } + + private static EventResult TryGetEvents(string head, string chunk) + { + string all = head + chunk; + var events = new List(); + var start = 0; + for (var i = 0; i < all.Length - 1; i++) + { + // message 之间以 \n\n 分隔 + if (all[i] == '\n' && i + 1 < all.Length && all[i + 1] == '\n') + { + var rawEvent = all.Substring(start, i - start).Trim(); + var sseEvent = ParseEvent(rawEvent); + events.Add(sseEvent); + start = i + 2; + i++; + } + } + string remain = all.Substring(start); + return new EventResult(events, remain); + } + + private static SSEEvent ParseEvent(string rawEvent) + { + var sseEvent = new SSEEvent(); + var lines = rawEvent.Split('\n'); + + foreach (var line in lines) + { + if (line.StartsWith(DATA_PREFIX)) + { + sseEvent.Data = line.Substring(DATA_PREFIX.Length).Trim(); + } + else if (line.StartsWith(EVENT_PREFIX)) + { + sseEvent.Event = line.Substring(EVENT_PREFIX.Length).Trim(); + } + else if (line.StartsWith(ID_PREFIX)) + { + sseEvent.Id = line.Substring(ID_PREFIX.Length).Trim(); + } + else if (line.StartsWith(RETRY_PREFIX)) + { + var retryData = line.Substring(RETRY_PREFIX.Length).Trim(); + int retryValue; + if (int.TryParse(retryData, out retryValue)) + { + sseEvent.Retry = retryValue; + } + } + else if (line.StartsWith(":")) + { + // ignore the line + } + } + + return sseEvent; + } + + + public static IEnumerable ReadAsSSE(Stream stream) + { + using (var reader = new StreamReader(stream)) + { + var buffer = new char[4096]; + var rest = string.Empty; + int count; + + while ((count = reader.Read(buffer, 0, buffer.Length)) > 0) + { + var chunk = new string(buffer, 0, count); + + var eventResult = TryGetEvents(rest, chunk); + rest = eventResult.Remain; + + if (eventResult.Events != null && eventResult.Events.Count > 0) + { + foreach (var @event in eventResult.Events) + { + yield return @event; + } + } + } + } + } + + +#if NETSTANDARD2_1 || NETCOREAPP3_0 + public static async IAsyncEnumerable ReadAsSSEAsync(Stream stream) + { + using var reader = new StreamReader(stream); + var buffer = new char[4096]; + var rest = string.Empty; + + int count; + while ((count = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + var chunk = new string(buffer, 0, count); + + var eventResult = TryGetEvents(rest, chunk); + rest = eventResult.Remain; + if (eventResult.Events != null && eventResult.Events.Count > 0) + { + foreach (var @event in eventResult.Events) + { + yield return @event; + } + } + } + } +#endif + } +} \ No newline at end of file diff --git a/Tea/Utils/ReadJsonUtil.cs b/Tea/Utils/ReadJsonUtil.cs new file mode 100644 index 0000000..faa5e3c --- /dev/null +++ b/Tea/Utils/ReadJsonUtil.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; + +using Newtonsoft.Json.Linq; + +namespace Tea.Utils +{ + internal static class ReadJsonUtil + { + internal static object Deserialize(object obj) + { + if (obj == null) + { + return null; + } + if (obj is JArray) + { + return DeserializeJArray((JArray) obj); + } + else if (obj is JObject) + { + return DeserializeJObject((JObject) obj); + } + else + { + return obj; + } + } + + private static Dictionary DeserializeJObject(JObject obj) + { + Dictionary dic = new Dictionary(); + Dictionary dicJObj = obj.ToObject>(); + foreach (var keypair in dicJObj) + { + dic.Add(keypair.Key, Deserialize(keypair.Value)); + } + return dic; + } + + private static List DeserializeJArray(JArray obj) + { + if (obj.Count == 0) + { + return new List(); + } + + if (obj[0].Type == JTokenType.Object) + { + List dicList = new List(); + List> dicObjList = obj.ToObject>>(); + foreach (Dictionary objItem in dicObjList) + { + Dictionary objDict = new Dictionary(); + foreach (var keypair in objItem) + { + objDict.Add(keypair.Key, Deserialize(keypair.Value)); + } + dicList.Add(objDict); + } + return dicList; + } + else if (obj[0].Type == JTokenType.Array) + { + List dicObjList = obj.ToObject>(); + List dicList = new List(); + foreach (var item in dicObjList) + { + dicList.Add(Deserialize((JArray) item)); + } + + return dicList; + } + else + { + List dicObjList = obj.ToObject>(); + return dicObjList; + } + } + } +} \ No newline at end of file diff --git a/Tea/tea.csproj b/Tea/tea.csproj index 43879c8..d355a0b 100644 --- a/Tea/tea.csproj +++ b/Tea/tea.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net45 + netstandard2.0;net45;netstandard2.1;netcoreapp3.0 Tea Library Alibaba Cloud @@ -27,6 +27,16 @@ NET45 + + NETSTANDARD2_1 + 8 + + + + NETCOREAPP3_0 + 8 + + diff --git a/TeaUnitTests/TeaStreamTest.cs b/TeaUnitTests/TeaStreamTest.cs new file mode 100644 index 0000000..824edbb --- /dev/null +++ b/TeaUnitTests/TeaStreamTest.cs @@ -0,0 +1,514 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +using Tea; + +using Xunit; +using System.Net; +using System.Net.Http; +using Newtonsoft.Json; +using System.Net.Security; +using System; + +namespace tests +{ + public class SseServer : IDisposable + { + private readonly HttpListener _httpListener; + private CancellationTokenSource _cancellationTokenSource; + + public SseServer(string uriPrefix) + { + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add(uriPrefix); + } + + public void Start() + { + _cancellationTokenSource = new CancellationTokenSource(); + _httpListener.Start(); + Task.Run(() => HandleIncomingConnections(_cancellationTokenSource.Token)); + } + + private async Task HandleIncomingConnections(CancellationToken cancellationToken) + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + try + { + var context = await _httpListener.GetContextAsync().ConfigureAwait(false); + + if (context.Request.Url?.AbsolutePath == "/sse") + { + HandleSseResponse(context.Response); + } + else if (context.Request.Url?.AbsolutePath == "/sse_with_no_spaces") + { + HandleSseWithNoSpacesResponse(context.Response); + } + else if (context.Request.Url?.AbsolutePath == "/sse_invalid_retry") + { + HandleSseWithInvalidRetryResponse(context.Response); + } + else if (context.Request.Url?.AbsolutePath == "/sse_with_data_divided") + { + HandleSseWithDataDividedResponse(context.Response); + } + } + catch (HttpListenerException) when (cancellationToken.IsCancellationRequested) + { + throw new HttpListenerException(); + } + } + } + + private void HandleSseResponse(HttpListenerResponse response) + { + int count = 0; + Timer timer = null; + timer = new Timer(_ => + { + if (count >= 5) + { + timer.Dispose(); + response.Close(); + return; + } + + byte[] buffer = Encoding.UTF8.GetBytes(string.Format("data: {0}\nevent: flow\nid: sse-test\nretry: 3\n:heartbeat\n\n", JsonConvert.SerializeObject(new { count = count }))); + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Flush(); + count++; + }, null, 0, 100); + } + + private void HandleSseWithNoSpacesResponse(HttpListenerResponse response) + { + int count = 0; + Timer timer = null; + timer = new Timer(_ => + { + if (count >= 5) + { + timer.Dispose(); + response.Close(); + return; + } + + byte[] buffer = Encoding.UTF8.GetBytes(string.Format("data: {0}\nevent:flow\nid:sse-test\nretry:3\n\n", JsonConvert.SerializeObject(new { count = count }))); + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Flush(); + count++; + }, null, 0, 100); + } + + private void HandleSseWithInvalidRetryResponse(HttpListenerResponse response) + { + int count = 0; + Timer timer = null; + timer = new Timer(_ => + { + if (count >= 5) + { + timer.Dispose(); + response.Close(); + return; + } + + byte[] buffer = Encoding.UTF8.GetBytes(string.Format("data: {0}\nevent:flow\nid:sse-test\nretry: abc\n\n", JsonConvert.SerializeObject(new { count = count }))); + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Flush(); + count++; + }, null, 0, 100); + } + + private void HandleSseWithDataDividedResponse(HttpListenerResponse response) + { + int count = 0; + Timer timer = null; + timer = new Timer(_ => + { + if (count >= 5) + { + timer.Dispose(); + response.Close(); + return; + } + + if (count == 1) + { + byte[] buffer = Encoding.UTF8.GetBytes("data:{\"count\":"); + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Flush(); + count++; + return; + } + + if (count == 2) + { + byte[] buffer = Encoding.UTF8.GetBytes(string.Format("{0},\"tag\":\"divided\"}}\nevent:flow\nid:sse-test\nretry:3\n\n", count++)); + response.OutputStream.Write(buffer, 0, buffer.Length); + response.OutputStream.Flush(); + return; + } + + byte[] buffer1 = Encoding.UTF8.GetBytes(string.Format("data: {0}\nevent:flow\nid:sse-test\nretry:3\n\n", JsonConvert.SerializeObject(new { count = count++ }))); + response.OutputStream.Write(buffer1, 0, buffer1.Length); + response.OutputStream.Flush(); + }, null, 0, 100); + } + + public void Stop() + { + _cancellationTokenSource.Cancel(); + _httpListener.Stop(); + _httpListener.Close(); + } + + public void Dispose() + { + Stop(); + ((IDisposable)_httpListener)?.Dispose(); + _cancellationTokenSource?.Dispose(); + } + } + + + public class TeaStreamTest : IAsyncLifetime + { + private SseServer server = new SseServer("http://localhost:8384/"); + + public async Task InitializeAsync() + { + server.Start(); + await Task.Delay(1000); + } + + public Task DisposeAsync() + { + server.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public void Test_ReadAsString() + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes("test"))) + { + Assert.Equal("test", TeaStream.ReadAsString(stream)); + } + } + + [Fact] + public async void Test_ReadAsStringAsync() + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes("test"))) + { + Assert.Equal("test", await TeaStream.ReadAsStringAsync(stream)); + } + } + + [Fact] + public void Test_ReadAsJSON() + { + string jsonStr = "{\"arrayObj\":[[{\"itemName\":\"item\",\"itemInt\":1},{\"itemName\":\"item2\",\"itemInt\":2}],[{\"itemName\":\"item3\",\"itemInt\":3}]],\"arrayList\":[[[1,2],[3,4]],[[5,6],[7]],[]],\"listStr\":[1,2,3],\"items\":[{\"total_size\":18,\"partNumber\":1,\"tags\":[{\"aa\":\"11\"}]},{\"total_size\":20,\"partNumber\":2,\"tags\":[{\"aa\":\"22\"}]}],\"next_marker\":\"\",\"test\":{\"total_size\":19,\"partNumber\":1,\"tags\":[{\"aa\":\"11\"}]}}"; + byte[] array = Encoding.UTF8.GetBytes(jsonStr); + using (MemoryStream stream = new MemoryStream(array)) + { + Dictionary dic = (Dictionary)TeaStream.ReadAsJSON(stream); + Assert.NotNull(dic); + List listResult = (List)dic["items"]; + Dictionary item1 = (Dictionary)listResult[0]; + Assert.Equal(18L, item1["total_size"]); + Assert.Empty((string)dic["next_marker"]); + Assert.Equal(2, ((List)dic["arrayObj"]).Count); + } + + jsonStr = "[{\"itemName\":\"item\",\"itemInt\":1},{\"itemName\":\"item2\",\"itemInt\":2}]"; + array = Encoding.UTF8.GetBytes(jsonStr); + using (MemoryStream stream = new MemoryStream(array)) + { + List listResult = (List)TeaStream.ReadAsJSON(stream); + Assert.NotNull(listResult); + Dictionary item1 = (Dictionary)listResult[0]; + Assert.Equal("item", item1["itemName"]); + Assert.Equal(1L, item1["itemInt"]); + } + } + + [Fact] + public async void Test_ReadAsJSONAsync() + { + string jsonStr = "{\"arrayObj\":[[{\"itemName\":\"item\",\"itemInt\":1},{\"itemName\":\"item2\",\"itemInt\":2}],[{\"itemName\":\"item3\",\"itemInt\":3}]],\"arrayList\":[[[1,2],[3,4]],[[5,6],[7]],[]],\"listStr\":[1,2,3],\"items\":[{\"total_size\":18,\"partNumber\":1,\"tags\":[{\"aa\":\"11\"}]},{\"total_size\":20,\"partNumber\":2,\"tags\":[{\"aa\":\"22\"}]}],\"next_marker\":\"\",\"test\":{\"total_size\":19,\"partNumber\":1,\"tags\":[{\"aa\":\"11\"}]}}"; + byte[] array = Encoding.UTF8.GetBytes(jsonStr); + using (MemoryStream stream = new MemoryStream(array)) + { + Dictionary dic = (Dictionary)await TeaStream.ReadAsJSONAsync(stream); + Assert.NotNull(dic); + List listResult = (List)dic["items"]; + Dictionary item1 = (Dictionary)listResult[0]; + Assert.Equal(18L, item1["total_size"]); + Assert.Empty((string)dic["next_marker"]); + Assert.Equal(2, ((List)dic["arrayObj"]).Count); + } + + jsonStr = "[{\"itemName\":\"item\",\"itemInt\":1},{\"itemName\":\"item2\",\"itemInt\":2}]"; + array = Encoding.UTF8.GetBytes(jsonStr); + using (MemoryStream stream = new MemoryStream(array)) + { + List listResult = (List)await TeaStream.ReadAsJSONAsync(stream); + Assert.NotNull(listResult); + Dictionary item1 = (Dictionary)listResult[0]; + Assert.Equal("item", item1["itemName"]); + Assert.Equal(1L, item1["itemInt"]); + } + } + + [Fact] + public void Test_ReadAsBytes() + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes("test"))) + { + Assert.NotNull(TeaStream.ReadAsBytes(stream)); + } + } + + [Fact] + public async void Test_ReadAsBytesAsync() + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes("test"))) + { + Assert.NotNull(await TeaStream.ReadAsBytesAsync(stream)); + } + } + + [Fact] + public async Task Test_ReadAsSSEAsync() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse"); + + var events = new List(); + + await foreach (var sseEvent in TeaStream.ReadAsSSEAsync(response)) + { + events.Add(sseEvent); + } + + for (int i = 0; i < 5; i++) + { + Assert.Equal(JsonConvert.SerializeObject(new { count = i }), events[i].Data); + Assert.Equal("sse-test", events[i].Id); + Assert.Equal("flow", events[i].Event); + Assert.Equal(3, events[i].Retry); + } + } + } + + [Fact] + public async Task Test_ReadAsSSEAsync_WithNoSpaces() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse_with_no_spaces"); + + var events = new List(); + + await foreach (var sseEvent in TeaStream.ReadAsSSEAsync(response)) + { + events.Add(sseEvent); + } + + Assert.Equal(5, events.Count); + + for (int i = 0; i < 5; i++) + { + Assert.Equal(JsonConvert.SerializeObject(new { count = i }), events[i].Data); + Assert.Equal("sse-test", events[i].Id); + Assert.Equal("flow", events[i].Event); + Assert.Equal(3, events[i].Retry); + } + } + } + + [Fact] + public async Task Test_ReadAsSSEAsync_WithInvalidRetry() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse_invalid_retry"); + + var events = new List(); + + await foreach (var sseEvent in TeaStream.ReadAsSSEAsync(response)) + { + events.Add(sseEvent); + } + + Assert.Equal(5, events.Count); + + for (int i = 0; i < 5; i++) + { + Assert.Equal(JsonConvert.SerializeObject(new { count = i }), events[i].Data); + Assert.Equal("sse-test", events[i].Id); + Assert.Equal("flow", events[i].Event); + Assert.Null(events[i].Retry); + } + } + } + + [Fact] + public async Task Test_ReadAsSSEAsync_WithDividedData() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse_with_data_divided"); + + var events = new List(); + + await foreach (var sseEvent in TeaStream.ReadAsSSEAsync(response)) + { + events.Add(sseEvent); + } + Assert.Equal(4, events.Count); + Assert.Equal(JsonConvert.SerializeObject(new { count = 0 }), events[0].Data); + Assert.Equal("sse-test", events[0].Id); + Assert.Equal("flow", events[0].Event); + Assert.Equal(3, events[0].Retry); + + Assert.Equal(JsonConvert.SerializeObject(new { count = 2, tag = "divided" }), events[1].Data); + Assert.Equal("sse-test", events[1].Id); + Assert.Equal("flow", events[1].Event); + Assert.Equal(3, events[1].Retry); + + Assert.Equal(JsonConvert.SerializeObject(new { count = 3 }), events[2].Data); + Assert.Equal("sse-test", events[2].Id); + Assert.Equal("flow", events[2].Event); + Assert.Equal(3, events[2].Retry); + + Assert.Equal(JsonConvert.SerializeObject(new { count = 4 }), events[3].Data); + Assert.Equal("sse-test", events[3].Id); + Assert.Equal("flow", events[3].Event); + Assert.Equal(3, events[3].Retry); + } + } + + [Fact] + public void Test_ReadAsSSE() + { + using (var client = new HttpClient()) + { + var response = client.GetStreamAsync("http://localhost:8384/sse").Result; + var events = new List(); + + foreach (var sseEvent in TeaStream.ReadAsSSE(response)) + { + events.Add(sseEvent); + } + + for (int i = 0; i < 5; i++) + { + Assert.Equal(JsonConvert.SerializeObject(new { count = i }), events[i].Data); + Assert.Equal("sse-test", events[i].Id); + Assert.Equal("flow", events[i].Event); + Assert.Equal(3, events[i].Retry); + } + } + } + + [Fact] + public async Task Test_ReadAsSSE_WithNoSpaces() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse_with_no_spaces"); + + var events = new List(); + + foreach (var sseEvent in TeaStream.ReadAsSSE(response)) + { + events.Add(sseEvent); + } + + Assert.Equal(5, events.Count); + + for (int i = 0; i < 5; i++) + { + Assert.Equal(JsonConvert.SerializeObject(new { count = i }), events[i].Data); + Assert.Equal("sse-test", events[i].Id); + Assert.Equal("flow", events[i].Event); + Assert.Equal(3, events[i].Retry); + } + } + } + + [Fact] + public async Task Test_ReadAsSSE_WithInvalidRetry() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse_invalid_retry"); + + var events = new List(); + + foreach (var sseEvent in TeaStream.ReadAsSSE(response)) + { + events.Add(sseEvent); + } + + Assert.Equal(5, events.Count); + + for (int i = 0; i < 5; i++) + { + Assert.Equal(JsonConvert.SerializeObject(new { count = i }), events[i].Data); + Assert.Equal("sse-test", events[i].Id); + Assert.Equal("flow", events[i].Event); + Assert.Null(events[i].Retry); + } + } + } + + [Fact] + public async Task Test_ReadAsSSE_WithDividedData() + { + using (var client = new HttpClient()) + { + var response = await client.GetStreamAsync("http://localhost:8384/sse_with_data_divided"); + + var events = new List(); + + foreach (var sseEvent in TeaStream.ReadAsSSE(response)) + { + events.Add(sseEvent); + } + + Assert.Equal(4, events.Count); + Assert.Equal(JsonConvert.SerializeObject(new { count = 0 }), events[0].Data); + Assert.Equal("sse-test", events[0].Id); + Assert.Equal("flow", events[0].Event); + Assert.Equal(3, events[0].Retry); + + Assert.Equal(JsonConvert.SerializeObject(new { count = 2, tag = "divided" }), events[1].Data); + Assert.Equal("sse-test", events[1].Id); + Assert.Equal("flow", events[1].Event); + Assert.Equal(3, events[1].Retry); + + Assert.Equal(JsonConvert.SerializeObject(new { count = 3 }), events[2].Data); + Assert.Equal("sse-test", events[2].Id); + Assert.Equal("flow", events[2].Event); + Assert.Equal(3, events[2].Retry); + + Assert.Equal(JsonConvert.SerializeObject(new { count = 4 }), events[3].Data); + Assert.Equal("sse-test", events[3].Id); + Assert.Equal("flow", events[3].Event); + Assert.Equal(3, events[3].Retry); + } + } + } +} + diff --git a/TeaUnitTests/TeaUnitTests.csproj b/TeaUnitTests/TeaUnitTests.csproj index e417cdc..fa09575 100644 --- a/TeaUnitTests/TeaUnitTests.csproj +++ b/TeaUnitTests/TeaUnitTests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp3.1 false false TeaUnitTests diff --git a/TeaUnitTests/Utils/ReadJsonUtilTest.cs b/TeaUnitTests/Utils/ReadJsonUtilTest.cs new file mode 100644 index 0000000..eef9245 --- /dev/null +++ b/TeaUnitTests/Utils/ReadJsonUtilTest.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +using Tea.Utils; + +using Newtonsoft.Json.Linq; + +using Xunit; + +namespace tests.Utils +{ + public class ReadJsonUtilTest + { + [Fact] + public void TestDeserializeToDic() + { + Assert.Null(ReadJsonUtil.Deserialize(null)); + + string jsonStr = "{\"arrayObj\":[[{\"itemName\":\"item\",\"itemInt\":1},{\"itemName\":\"item2\",\"itemInt\":2}],[{\"itemName\":\"item3\",\"itemInt\":3}]],\"arrayList\":[[[1,2],[3,4]],[[5,6],[7]],[]],\"listStr\":[1,2,3],\"items\":[{\"total_size\":18,\"partNumber\":1,\"tags\":[{\"aa\":\"11\"}]},{\"total_size\":20,\"partNumber\":2,\"tags\":[{\"aa\":\"22\"}]}],\"next_marker\":\"\",\"test\":{\"total_size\":19,\"partNumber\":1,\"tags\":[{\"aa\":\"11\"}]}}"; + JObject jObject = JObject.Parse(jsonStr); + Dictionary dic = (Dictionary) ReadJsonUtil.Deserialize(jObject); + Assert.NotNull(dic); + List listResult = (List) dic["items"]; + Dictionary item1 = (Dictionary) listResult[0]; + Assert.Equal(18L, item1["total_size"]); + Assert.Empty((string) dic["next_marker"]); + Assert.Equal(2, ((List) dic["arrayObj"]).Count); + } + + } +}