From 472e12682024307a236e0e232c283fee3d283468 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 13:27:18 +0400 Subject: [PATCH 01/13] feat(repeater): establish bridges integration closes #170 --- src/SecTester.Core/Bus/Message.cs | 6 + src/SecTester.Core/Configuration.cs | 2 +- .../Bus/DefaultRepeaterBusFactory.cs | 3 +- .../MessagePackHttpHeadersFormatter.cs | 162 ++++++++++++++++++ .../MessagePackHttpMethodFormatter.cs | 36 ++++ .../SocketIOIncomingRequestMapper.cs | 84 +++++++++ src/SecTester.Repeater/Bus/IncomingRequest.cs | 14 +- .../Bus/OutgoingResponse.cs | 15 +- src/SecTester.Repeater/Bus/RepeaterError.cs | 1 + src/SecTester.Repeater/Bus/RepeaterInfo.cs | 3 +- src/SecTester.Repeater/Bus/RepeaterVersion.cs | 1 + src/SecTester.Repeater/Bus/SocketIoMessage.cs | 13 +- .../SecTester.Repeater.csproj | 3 +- src/SecTester.Repeater/packages.lock.json | 8 +- src/SecTester.Runner/packages.lock.json | 18 +- .../packages.lock.json | 12 +- .../SecTester.Runner.Tests/packages.lock.json | 24 +-- 17 files changed, 366 insertions(+), 39 deletions(-) create mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs create mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs create mode 100644 src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs diff --git a/src/SecTester.Core/Bus/Message.cs b/src/SecTester.Core/Bus/Message.cs index efaac6f..51272ab 100644 --- a/src/SecTester.Core/Bus/Message.cs +++ b/src/SecTester.Core/Bus/Message.cs @@ -1,12 +1,18 @@ using System; +using System.Runtime.Serialization; using SecTester.Core.Utils; namespace SecTester.Core.Bus; public abstract record Message { + [IgnoreDataMember] public string CorrelationId { get; protected init; } + + [IgnoreDataMember] public DateTime CreatedAt { get; protected init; } + + [IgnoreDataMember] public string Type { get; protected init; } protected Message() diff --git a/src/SecTester.Core/Configuration.cs b/src/SecTester.Core/Configuration.cs index 293a636..c9a05e8 100644 --- a/src/SecTester.Core/Configuration.cs +++ b/src/SecTester.Core/Configuration.cs @@ -92,7 +92,7 @@ private void ResolveUrls(Uri uri) if (_loopbackAddresses.Any(address => address == host)) { Bus = $"amqp://{host}:5672"; - Api = $"http://{host}:8000"; + Api = $"http://{host}:8090"; } else { diff --git a/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs b/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs index 45f6a56..db3ae74 100644 --- a/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs +++ b/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs @@ -5,6 +5,7 @@ using SecTester.Core.Utils; using SocketIO.Serializer.MessagePack; using SocketIOClient; +using SocketIOClient.Transport; namespace SecTester.Repeater.Bus; @@ -37,7 +38,7 @@ public IRepeaterBus Create(string repeaterId) ReconnectionAttempts = options.ReconnectionAttempts, ReconnectionDelayMax = options.ReconnectionDelayMax, ConnectionTimeout = options.ConnectionTimeout, - AutoUpgrade = false, + Transport = TransportProtocol.WebSocket, Auth = new { token = _config.Credentials.Token, domain = repeaterId } }) { diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs new file mode 100644 index 0000000..7cddbf2 --- /dev/null +++ b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Bus.Formatters; + +// Headers formatter is to be support javascript `undefined` which is treated as null (0xC0) +// https://www.npmjs.com/package/@msgpack/msgpack#messagepack-mapping-table +// https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format + +public class MessagePackHttpHeadersFormatter: IMessagePackFormatter< + IEnumerable>> +> +{ + public MessagePackHttpHeadersFormatter() + { + // noop + } + + public void Serialize(ref MessagePackWriter writer, IEnumerable>> value, + MessagePackSerializerOptions options) + { + + if (value == null) + { + writer.WriteNil(); + } + else + { + var count = value.Count(); + + writer.WriteMapHeader(count); + + SerializeMap(ref writer, value); + } + } + + + + public IEnumerable>> Deserialize(ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + switch (reader.NextMessagePackType) + { + case MessagePackType.Nil: + return null; + case MessagePackType.Map: + break; + default: + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, + "Unrecognized code: 0x{0:X2} but expected to be a map or null", reader.NextCode)); + } + + var length = reader.ReadMapHeader(); + + options.Security.DepthStep(ref reader); + + try + { + return DeserializeMap(ref reader, length, options); + } + finally + { + reader.Depth--; + } + } + + private static void SerializeMap(ref MessagePackWriter writer, IEnumerable>> value) + { + + foreach (var item in value) + { + writer.Write(item.Key); + + var headersCount = item.Value.Count(); + + if (headersCount == 1) + { + writer.Write(item.Value.First()); + } + else + { + writer.WriteArrayHeader(headersCount); + + foreach (var subItem in item.Value) + { + writer.Write(subItem); + } + } + }; + } + + + private static List>> DeserializeMap(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) + { + var result = new List>>(length); + + for ( int i = 0 ; i < length ; i++ ) + { + var key = DeserializeString(ref reader); + + switch (reader.NextMessagePackType) + { + case MessagePackType.String: + result.Add(new KeyValuePair>(key, new List{DeserializeString(ref reader)})); + break; + case MessagePackType.Array: + result.Add(new KeyValuePair>(key, DeserializeArray(ref reader, reader.ReadArrayHeader(), options))); + break; + default: + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Unrecognized code: 0x{0:X2} but expected to be either a string or an array.", reader.NextCode)); + } + } + + + return result; + } + + private static IEnumerable DeserializeArray(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) + { + var result = new List(length); + + if (length == 0) + { + return result; + } + + options.Security.DepthStep(ref reader); + try + { + for ( int i = 0 ; i < length ; i++ ) + { + result.Add(DeserializeString(ref reader)); + } + } + finally + { + reader.Depth--; + } + + return result; + } + + private static string DeserializeString(ref MessagePackReader reader) + { + if (reader.NextMessagePackType != MessagePackType.String) + { + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Unrecognized code: 0x{0:X2} but expected to be a string.", reader.NextCode)); + } + + var value = reader.ReadString(); + + if (null == value) + { + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Nulls are not allowed.")); + } + + return value; + } +} diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs new file mode 100644 index 0000000..12cb5eb --- /dev/null +++ b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using System.Net.Http; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Bus.Formatters; + +public class MessagePackHttpMethodFormatter : IMessagePackFormatter +{ + public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePackSerializerOptions options) + { + if (null == value) + { + writer.WriteNil(); + } + else + { + writer.Write(value.Method); + } + } + + public HttpMethod? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + switch (reader.NextMessagePackType) + { + case MessagePackType.Nil: + return null; + case MessagePackType.String: + var method = reader.ReadString(); + return string.IsNullOrWhiteSpace(method) ? null : new HttpMethod(method); + default: + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, + "Unrecognized code: 0x{0:X2} but expected to be either a string or null.", reader.NextCode)); + } + } +} diff --git a/src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs b/src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs new file mode 100644 index 0000000..e413f0e --- /dev/null +++ b/src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; + +namespace SecTester.Repeater.Bus.Formatters; + +public class SocketIOIncomingRequestMapper +{ + private const string Protocol = "protocol"; + private const string Url = "url"; + private const string Method = "method"; + private const string Body = "body"; + private const string Headers = "headers"; + + public static IncomingRequest ToRequest(Dictionary dictionary) + { + var protocol = dictionary.TryGetValue(Protocol, out var p) && p is string && Enum.TryParse(p.ToString(), out var e) + ? e + : SecTester.Repeater.Protocol.Http; + + var uri = dictionary.TryGetValue(Url, out var u) && u is string + ? new Uri(u.ToString()) + : throw new InvalidDataException(FormatPropertyError(Url)); + + var method = dictionary.TryGetValue(Method, out var m) && m is string + ? new HttpMethod(m.ToString()) + : HttpMethod.Get; + + var body = dictionary.TryGetValue(Body, out var b) && b is string ? b.ToString() : null; + + dictionary.TryGetValue(Headers, out var headers); + + return new IncomingRequest(uri) + { + Protocol = protocol, + Body = body, + Method = method, + Headers = MapHeaders(headers as Dictionary) + }; + } + + private static IEnumerable>> MapHeaders(Dictionary? headers) + { + var result = new List>>(headers?.Count ?? 0); + + if (null == headers) + { + return result; + } + + foreach (var kvp in headers) + { + var key = kvp.Key.ToString(); + + if (null == kvp.Value) + { + result.Add(new KeyValuePair>(key, new List())); + continue; + } + + if (kvp.Value is string) + { + result.Add(new KeyValuePair>(key, new List + { kvp.Value.ToString() })); + continue; + } + + if (kvp.Value is not object[] objects) + { + continue; + } + + var values = objects.OfType().Select(value => value.ToString()).ToList(); + + result.Add(new KeyValuePair>(key, values)); + } + + return result; + } + + private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type"; +} diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index da65e3b..addf05d 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -3,17 +3,29 @@ using System.Net.Http; using MessagePack; using SecTester.Core.Bus; +using SecTester.Repeater.Bus.Formatters; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public record IncomingRequest(Uri Url) : Event, IRequest { + [Key("body")] public string? Body { get; set; } + + [Key("method")] + [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] public HttpMethod Method { get; set; } = HttpMethod.Get; + + [Key("protocol")] public Protocol Protocol { get; set; } = Protocol.Http; + + [Key("url")] public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); + + [Key("headers")] + [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] public IEnumerable>> Headers { get; set; } = new List>>(); } diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index 6a6dd3e..a46a818 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -1,17 +1,30 @@ using System.Collections.Generic; using MessagePack; +using SecTester.Repeater.Bus.Formatters; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public record OutgoingResponse : IResponse { + [Key("statusCode")] public int? StatusCode { get; set; } + + [Key("body")] public string? Body { get; set; } + + [Key("message")] public string? Message { get; set; } + + [Key("errorCode")] public string? ErrorCode { get; set; } + + [Key("protocol")] public Protocol Protocol { get; set; } = Protocol.Http; + + [Key("headers")] + [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] public IEnumerable>> Headers { get; set; } = new List>>(); } diff --git a/src/SecTester.Repeater/Bus/RepeaterError.cs b/src/SecTester.Repeater/Bus/RepeaterError.cs index eabc9e6..87430df 100644 --- a/src/SecTester.Repeater/Bus/RepeaterError.cs +++ b/src/SecTester.Repeater/Bus/RepeaterError.cs @@ -5,5 +5,6 @@ namespace SecTester.Repeater.Bus; [MessagePackObject(true)] public sealed record RepeaterError { + [Key("message")] public string Message { get; set; } = null!; } diff --git a/src/SecTester.Repeater/Bus/RepeaterInfo.cs b/src/SecTester.Repeater/Bus/RepeaterInfo.cs index b76257b..4671e90 100644 --- a/src/SecTester.Repeater/Bus/RepeaterInfo.cs +++ b/src/SecTester.Repeater/Bus/RepeaterInfo.cs @@ -2,8 +2,9 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public sealed record RepeaterInfo { + [Key("repeaterId")] public string RepeaterId { get; set; } = null!; } diff --git a/src/SecTester.Repeater/Bus/RepeaterVersion.cs b/src/SecTester.Repeater/Bus/RepeaterVersion.cs index 915aa21..373afa9 100644 --- a/src/SecTester.Repeater/Bus/RepeaterVersion.cs +++ b/src/SecTester.Repeater/Bus/RepeaterVersion.cs @@ -5,5 +5,6 @@ namespace SecTester.Repeater.Bus; [MessagePackObject(true)] public sealed record RepeaterVersion { + [Key("version")] public string Version { get; set; } = null!; } diff --git a/src/SecTester.Repeater/Bus/SocketIoMessage.cs b/src/SecTester.Repeater/Bus/SocketIoMessage.cs index d336db1..6527bfb 100644 --- a/src/SecTester.Repeater/Bus/SocketIoMessage.cs +++ b/src/SecTester.Repeater/Bus/SocketIoMessage.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using SecTester.Repeater.Bus.Formatters; using SocketIOClient; namespace SecTester.Repeater.Bus; @@ -14,7 +16,16 @@ public SocketIoMessage(SocketIOResponse response) _response = response ?? throw new ArgumentNullException(nameof(response)); } - public virtual T GetValue(int index = 0) => _response.GetValue(index); + public virtual T GetValue(int index = 0) + { + if (typeof(T) == typeof(IncomingRequest)) + { + var raw = _response.GetValue>(index); + return (T)(object)SocketIOIncomingRequestMapper.ToRequest(raw); + } + + return _response.GetValue(index); + } public virtual Task CallbackAsync(params object[] data) => _response.CallbackAsync(data); diff --git a/src/SecTester.Repeater/SecTester.Repeater.csproj b/src/SecTester.Repeater/SecTester.Repeater.csproj index 15eb7f1..8468a26 100644 --- a/src/SecTester.Repeater/SecTester.Repeater.csproj +++ b/src/SecTester.Repeater/SecTester.Repeater.csproj @@ -12,12 +12,11 @@ - + - diff --git a/src/SecTester.Repeater/packages.lock.json b/src/SecTester.Repeater/packages.lock.json index f4301ee..149aa87 100644 --- a/src/SecTester.Repeater/packages.lock.json +++ b/src/SecTester.Repeater/packages.lock.json @@ -23,9 +23,9 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Direct", - "requested": "[3.1.1, )", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "requested": "[3.1.2, )", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -415,7 +415,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } diff --git a/src/SecTester.Runner/packages.lock.json b/src/SecTester.Runner/packages.lock.json index 0213610..ae4d9cd 100644 --- a/src/SecTester.Runner/packages.lock.json +++ b/src/SecTester.Runner/packages.lock.json @@ -231,8 +231,8 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -412,7 +412,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } @@ -429,9 +429,9 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", - "SocketIO.Serializer.MessagePack": "[3.1.1, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", + "SocketIO.Serializer.MessagePack": "[3.1.2, )", "SocketIOClient": "[3.1.1, )", "System.Linq.Async": "[6.0.1, )" } @@ -440,7 +440,7 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Scan": "[0.40.0, )" + "SecTester.Scan": "[0.41.3, )" } }, "sectester.scan": { @@ -448,8 +448,8 @@ "dependencies": { "Macross.Json.Extensions": "[3.0.0, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", "System.Linq.Async": "[6.0.1, )", "System.Text.Json": "[6.0.0, )" } diff --git a/test/SecTester.Repeater.Tests/packages.lock.json b/test/SecTester.Repeater.Tests/packages.lock.json index d7051cc..4765948 100644 --- a/test/SecTester.Repeater.Tests/packages.lock.json +++ b/test/SecTester.Repeater.Tests/packages.lock.json @@ -695,8 +695,8 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -1589,7 +1589,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } @@ -1606,9 +1606,9 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", - "SocketIO.Serializer.MessagePack": "[3.1.1, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", + "SocketIO.Serializer.MessagePack": "[3.1.2, )", "SocketIOClient": "[3.1.1, )", "System.Linq.Async": "[6.0.1, )" } diff --git a/test/SecTester.Runner.Tests/packages.lock.json b/test/SecTester.Runner.Tests/packages.lock.json index 5ac24be..50f3db7 100644 --- a/test/SecTester.Runner.Tests/packages.lock.json +++ b/test/SecTester.Runner.Tests/packages.lock.json @@ -508,8 +508,8 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -1404,7 +1404,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } @@ -1421,9 +1421,9 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", - "SocketIO.Serializer.MessagePack": "[3.1.1, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", + "SocketIO.Serializer.MessagePack": "[3.1.2, )", "SocketIOClient": "[3.1.1, )", "System.Linq.Async": "[6.0.1, )" } @@ -1432,16 +1432,16 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Scan": "[0.40.0, )" + "SecTester.Scan": "[0.41.3, )" } }, "sectester.runner": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Repeater": "[0.40.0, )", - "SecTester.Reporter": "[0.40.0, )", - "SecTester.Scan": "[0.40.0, )" + "SecTester.Repeater": "[0.41.3, )", + "SecTester.Reporter": "[0.41.3, )", + "SecTester.Scan": "[0.41.3, )" } }, "sectester.scan": { @@ -1449,8 +1449,8 @@ "dependencies": { "Macross.Json.Extensions": "[3.0.0, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", "System.Linq.Async": "[6.0.1, )", "System.Text.Json": "[6.0.0, )" } From 373f3d6a88e3af247ccdfe1e828acbfdbe5e204e Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 13:30:02 +0400 Subject: [PATCH 02/13] feat(repeater): revert accidental modification closes #170 --- src/SecTester.Core/Configuration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SecTester.Core/Configuration.cs b/src/SecTester.Core/Configuration.cs index c9a05e8..293a636 100644 --- a/src/SecTester.Core/Configuration.cs +++ b/src/SecTester.Core/Configuration.cs @@ -92,7 +92,7 @@ private void ResolveUrls(Uri uri) if (_loopbackAddresses.Any(address => address == host)) { Bus = $"amqp://{host}:5672"; - Api = $"http://{host}:8090"; + Api = $"http://{host}:8000"; } else { From 3d86749042ba598cfd6e71354f6dd0f42e837a4a Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 15:21:47 +0400 Subject: [PATCH 03/13] feat(repeater): address complexity and duplication issues closes #170 --- .../MessagePackHttpHeadersFormatter.cs | 65 +++++++------- .../SocketIOIncomingRequestMapper.cs | 84 ------------------- src/SecTester.Repeater/Bus/HttpMessage.cs | 23 +++++ src/SecTester.Repeater/Bus/IncomingRequest.cs | 77 ++++++++++++++--- .../Bus/OutgoingResponse.cs | 15 +--- src/SecTester.Repeater/Bus/RepeaterError.cs | 2 +- src/SecTester.Repeater/Bus/RepeaterVersion.cs | 2 +- src/SecTester.Repeater/Bus/SocketIoMessage.cs | 6 +- 8 files changed, 125 insertions(+), 149 deletions(-) delete mode 100644 src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs create mode 100644 src/SecTester.Repeater/Bus/HttpMessage.cs diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs index 7cddbf2..5e835a9 100644 --- a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs +++ b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs @@ -10,19 +10,13 @@ namespace SecTester.Repeater.Bus.Formatters; // https://www.npmjs.com/package/@msgpack/msgpack#messagepack-mapping-table // https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format -public class MessagePackHttpHeadersFormatter: IMessagePackFormatter< - IEnumerable>> +public class MessagePackHttpHeadersFormatter : IMessagePackFormatter< + IEnumerable>>? > { - public MessagePackHttpHeadersFormatter() - { - // noop - } - - public void Serialize(ref MessagePackWriter writer, IEnumerable>> value, + public void Serialize(ref MessagePackWriter writer, IEnumerable>>? value, MessagePackSerializerOptions options) { - if (value == null) { writer.WriteNil(); @@ -37,9 +31,7 @@ public void Serialize(ref MessagePackWriter writer, IEnumerable>> Deserialize(ref MessagePackReader reader, + public IEnumerable>>? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { switch (reader.NextMessagePackType) @@ -69,48 +61,53 @@ public IEnumerable>> Deserialize(ref Me private static void SerializeMap(ref MessagePackWriter writer, IEnumerable>> value) { - foreach (var item in value) { writer.Write(item.Key); - var headersCount = item.Value.Count(); + SerializeValue(ref writer, item); + } + } - if (headersCount == 1) - { - writer.Write(item.Value.First()); - } - else - { - writer.WriteArrayHeader(headersCount); + private static void SerializeValue(ref MessagePackWriter writer, KeyValuePair> item) + { + var headersCount = item.Value.Count(); + + if (headersCount == 1) + { + writer.Write(item.Value.First()); + } + else + { + writer.WriteArrayHeader(headersCount); - foreach (var subItem in item.Value) - { - writer.Write(subItem); - } + foreach (var subItem in item.Value) + { + writer.Write(subItem); } - }; + } } - - private static List>> DeserializeMap(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) + private static List>> DeserializeMap(ref MessagePackReader reader, int length, + MessagePackSerializerOptions options) { var result = new List>>(length); - for ( int i = 0 ; i < length ; i++ ) + for (var i = 0 ; i < length ; i++) { var key = DeserializeString(ref reader); switch (reader.NextMessagePackType) { case MessagePackType.String: - result.Add(new KeyValuePair>(key, new List{DeserializeString(ref reader)})); + result.Add(new KeyValuePair>(key, new List { DeserializeString(ref reader) })); break; case MessagePackType.Array: result.Add(new KeyValuePair>(key, DeserializeArray(ref reader, reader.ReadArrayHeader(), options))); break; default: - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Unrecognized code: 0x{0:X2} but expected to be either a string or an array.", reader.NextCode)); + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, + "Unrecognized code: 0x{0:X2} but expected to be either a string or an array.", reader.NextCode)); } } @@ -128,9 +125,10 @@ private static IEnumerable DeserializeArray(ref MessagePackReader reader } options.Security.DepthStep(ref reader); + try { - for ( int i = 0 ; i < length ; i++ ) + for (var i = 0 ; i < length ; i++) { result.Add(DeserializeString(ref reader)); } @@ -147,7 +145,8 @@ private static string DeserializeString(ref MessagePackReader reader) { if (reader.NextMessagePackType != MessagePackType.String) { - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Unrecognized code: 0x{0:X2} but expected to be a string.", reader.NextCode)); + throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, + "Unrecognized code: 0x{0:X2} but expected to be a string.", reader.NextCode)); } var value = reader.ReadString(); diff --git a/src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs b/src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs deleted file mode 100644 index e413f0e..0000000 --- a/src/SecTester.Repeater/Bus/Formatters/SocketIOIncomingRequestMapper.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; - -namespace SecTester.Repeater.Bus.Formatters; - -public class SocketIOIncomingRequestMapper -{ - private const string Protocol = "protocol"; - private const string Url = "url"; - private const string Method = "method"; - private const string Body = "body"; - private const string Headers = "headers"; - - public static IncomingRequest ToRequest(Dictionary dictionary) - { - var protocol = dictionary.TryGetValue(Protocol, out var p) && p is string && Enum.TryParse(p.ToString(), out var e) - ? e - : SecTester.Repeater.Protocol.Http; - - var uri = dictionary.TryGetValue(Url, out var u) && u is string - ? new Uri(u.ToString()) - : throw new InvalidDataException(FormatPropertyError(Url)); - - var method = dictionary.TryGetValue(Method, out var m) && m is string - ? new HttpMethod(m.ToString()) - : HttpMethod.Get; - - var body = dictionary.TryGetValue(Body, out var b) && b is string ? b.ToString() : null; - - dictionary.TryGetValue(Headers, out var headers); - - return new IncomingRequest(uri) - { - Protocol = protocol, - Body = body, - Method = method, - Headers = MapHeaders(headers as Dictionary) - }; - } - - private static IEnumerable>> MapHeaders(Dictionary? headers) - { - var result = new List>>(headers?.Count ?? 0); - - if (null == headers) - { - return result; - } - - foreach (var kvp in headers) - { - var key = kvp.Key.ToString(); - - if (null == kvp.Value) - { - result.Add(new KeyValuePair>(key, new List())); - continue; - } - - if (kvp.Value is string) - { - result.Add(new KeyValuePair>(key, new List - { kvp.Value.ToString() })); - continue; - } - - if (kvp.Value is not object[] objects) - { - continue; - } - - var values = objects.OfType().Select(value => value.ToString()).ToList(); - - result.Add(new KeyValuePair>(key, values)); - } - - return result; - } - - private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type"; -} diff --git a/src/SecTester.Repeater/Bus/HttpMessage.cs b/src/SecTester.Repeater/Bus/HttpMessage.cs new file mode 100644 index 0000000..08a4de6 --- /dev/null +++ b/src/SecTester.Repeater/Bus/HttpMessage.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using MessagePack; +using SecTester.Repeater.Bus.Formatters; + +namespace SecTester.Repeater.Bus; + +public record HttpMessage +{ + public const string HeadersKey = "headers"; + public const string BodyKey = "body"; + public const string ProtocolKey = "protocol"; + + [Key(ProtocolKey)] + public Protocol Protocol { get; set; } = Protocol.Http; + + [Key(HeadersKey)] + [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] + public IEnumerable>> Headers { get; set; } = + new List>>(); + + [Key(BodyKey)] + public string? Body { get; set; } +} diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index addf05d..3c8913c 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -1,31 +1,82 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net.Http; using MessagePack; -using SecTester.Core.Bus; using SecTester.Repeater.Bus.Formatters; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; [MessagePackObject] -public record IncomingRequest(Uri Url) : Event, IRequest +public record IncomingRequest(Uri Url): HttpMessage, IRequest { - [Key("body")] - public string? Body { get; set; } + private const string UrlKey = "url"; + private const string MethodKey = "method"; - [Key("method")] + [Key(MethodKey)] [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] public HttpMethod Method { get; set; } = HttpMethod.Get; - [Key("protocol")] - public Protocol Protocol { get; set; } = Protocol.Http; - - [Key("url")] + [Key(UrlKey)] public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); - [Key("headers")] - [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] - public IEnumerable>> Headers { get; set; } = - new List>>(); + public static IncomingRequest FromDictionary(Dictionary dictionary) + { + var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && Enum.TryParse(p.ToString(), out var e) + ? e + : Protocol.Http; + + var uri = dictionary.TryGetValue(UrlKey, out var u) && u is string + ? new Uri(u.ToString()) + : throw new InvalidDataException(FormatPropertyError(UrlKey)); + + var method = dictionary.TryGetValue(MethodKey, out var m) && m is string + ? new HttpMethod(m.ToString()) + : HttpMethod.Get; + + var body = dictionary.TryGetValue(BodyKey, out var b) && b is string ? b.ToString() : null; + + var headers = dictionary.TryGetValue(HeadersKey, out var h) && h is Dictionary value + ? MapHeaders(value) + : new List>>(); + + return new IncomingRequest(uri) + { + Protocol = protocol, + Body = body, + Method = method, + Headers = headers + }; + } + + private static IEnumerable>> MapHeaders(Dictionary headers) + { + var result = new List>>(headers?.Count ?? 0); + + foreach (var kvp in headers) + { + var key = kvp.Key.ToString(); + + switch (kvp.Value) + { + case null: + result.Add(new KeyValuePair>(key, new List())); + continue; + case string: + result.Add(new KeyValuePair>(key, new List + { kvp.Value.ToString() })); + continue; + case object[] objects: + result.Add(new KeyValuePair>(key, + objects.OfType().Select(value => value.ToString()).ToList())); + continue; + } + } + + return result; + } + + private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type"; } diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index a46a818..a56eca7 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -1,30 +1,17 @@ -using System.Collections.Generic; using MessagePack; -using SecTester.Repeater.Bus.Formatters; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; [MessagePackObject] -public record OutgoingResponse : IResponse +public record OutgoingResponse : HttpMessage, IResponse { [Key("statusCode")] public int? StatusCode { get; set; } - [Key("body")] - public string? Body { get; set; } - [Key("message")] public string? Message { get; set; } [Key("errorCode")] public string? ErrorCode { get; set; } - - [Key("protocol")] - public Protocol Protocol { get; set; } = Protocol.Http; - - [Key("headers")] - [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] - public IEnumerable>> Headers { get; set; } = - new List>>(); } diff --git a/src/SecTester.Repeater/Bus/RepeaterError.cs b/src/SecTester.Repeater/Bus/RepeaterError.cs index 87430df..ea76ab2 100644 --- a/src/SecTester.Repeater/Bus/RepeaterError.cs +++ b/src/SecTester.Repeater/Bus/RepeaterError.cs @@ -2,7 +2,7 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public sealed record RepeaterError { [Key("message")] diff --git a/src/SecTester.Repeater/Bus/RepeaterVersion.cs b/src/SecTester.Repeater/Bus/RepeaterVersion.cs index 373afa9..f3555a4 100644 --- a/src/SecTester.Repeater/Bus/RepeaterVersion.cs +++ b/src/SecTester.Repeater/Bus/RepeaterVersion.cs @@ -2,7 +2,7 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public sealed record RepeaterVersion { [Key("version")] diff --git a/src/SecTester.Repeater/Bus/SocketIoMessage.cs b/src/SecTester.Repeater/Bus/SocketIoMessage.cs index 6527bfb..ca704e3 100644 --- a/src/SecTester.Repeater/Bus/SocketIoMessage.cs +++ b/src/SecTester.Repeater/Bus/SocketIoMessage.cs @@ -20,8 +20,7 @@ public virtual T GetValue(int index = 0) { if (typeof(T) == typeof(IncomingRequest)) { - var raw = _response.GetValue>(index); - return (T)(object)SocketIOIncomingRequestMapper.ToRequest(raw); + return (T)(object)IncomingRequest.FromDictionary(_response.GetValue>(index)); } return _response.GetValue(index); @@ -29,5 +28,6 @@ public virtual T GetValue(int index = 0) public virtual Task CallbackAsync(params object[] data) => _response.CallbackAsync(data); - public virtual Task CallbackAsync(CancellationToken cancellationToken, params object[] data) => _response.CallbackAsync(cancellationToken, data); + public virtual Task CallbackAsync(CancellationToken cancellationToken, params object[] data) => + _response.CallbackAsync(cancellationToken, data); } From 6838353e22d8467d19cc53dfb31cefe1d691d13f Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 16:26:33 +0400 Subject: [PATCH 04/13] feat(repeater): improve coverage closes #170 --- src/SecTester.Core/Bus/Message.cs | 3 - src/SecTester.Repeater/Bus/HttpMessage.cs | 2 +- src/SecTester.Repeater/Bus/SocketIoMessage.cs | 3 +- .../MessagePackHttpHeadersFormatterTests.cs | 79 ++++++++++++++++ .../MessagePackHttpMethodFormatterTests.cs | 73 ++++++++++++++ .../Bus/IncomingRequestTests.cs | 94 +++++++++++++++++++ 6 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs create mode 100644 test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs create mode 100644 test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs diff --git a/src/SecTester.Core/Bus/Message.cs b/src/SecTester.Core/Bus/Message.cs index 51272ab..99a187f 100644 --- a/src/SecTester.Core/Bus/Message.cs +++ b/src/SecTester.Core/Bus/Message.cs @@ -6,13 +6,10 @@ namespace SecTester.Core.Bus; public abstract record Message { - [IgnoreDataMember] public string CorrelationId { get; protected init; } - [IgnoreDataMember] public DateTime CreatedAt { get; protected init; } - [IgnoreDataMember] public string Type { get; protected init; } protected Message() diff --git a/src/SecTester.Repeater/Bus/HttpMessage.cs b/src/SecTester.Repeater/Bus/HttpMessage.cs index 08a4de6..05fb3ae 100644 --- a/src/SecTester.Repeater/Bus/HttpMessage.cs +++ b/src/SecTester.Repeater/Bus/HttpMessage.cs @@ -4,7 +4,7 @@ namespace SecTester.Repeater.Bus; -public record HttpMessage +public abstract record HttpMessage { public const string HeadersKey = "headers"; public const string BodyKey = "body"; diff --git a/src/SecTester.Repeater/Bus/SocketIoMessage.cs b/src/SecTester.Repeater/Bus/SocketIoMessage.cs index ca704e3..839825e 100644 --- a/src/SecTester.Repeater/Bus/SocketIoMessage.cs +++ b/src/SecTester.Repeater/Bus/SocketIoMessage.cs @@ -28,6 +28,5 @@ public virtual T GetValue(int index = 0) public virtual Task CallbackAsync(params object[] data) => _response.CallbackAsync(data); - public virtual Task CallbackAsync(CancellationToken cancellationToken, params object[] data) => - _response.CallbackAsync(cancellationToken, data); + public virtual Task CallbackAsync(CancellationToken cancellationToken, params object[] data) => _response.CallbackAsync(cancellationToken, data); } diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs new file mode 100644 index 0000000..23b7b5d --- /dev/null +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs @@ -0,0 +1,79 @@ +using MessagePack; +using SecTester.Repeater.Bus.Formatters; + +namespace SecTester.Repeater.Tests.Bus.Formatters; + +public sealed class MessagePackHttpHeadersFormatterTests +{ + [MessagePackObject] + public record HttpHeadersDto + { + [Key("headers")] + [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] + public IEnumerable>>? Headers { get; set; } + } + + private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + + private static IEnumerable< + HttpHeadersDto> + Fixtures => + new[] + { + new HttpHeadersDto + { + Headers = null + }, + new HttpHeadersDto + { + Headers = new List>>() + }, + + new HttpHeadersDto + { + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + } + } + }; + + public static IEnumerable SerializeDeserializeFixtures => Fixtures + .Select((x) => new object?[] + { + x, x + }); + + [Theory] + [MemberData(nameof(SerializeDeserializeFixtures))] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( + HttpHeadersDto input, + HttpHeadersDto expected) + { + // arrange + var serialized = MessagePackSerializer.Serialize(input, Options); + + // act + var result = MessagePackSerializer.Deserialize(serialized, Options); + + // assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson("{}", Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().BeEquivalentTo(new HttpHeadersDto + { + Headers = null + }); + } +} diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs new file mode 100644 index 0000000..b4d30f9 --- /dev/null +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs @@ -0,0 +1,73 @@ +using MessagePack; +using SecTester.Repeater.Bus.Formatters; + +namespace SecTester.Repeater.Tests.Bus.Formatters; + +public sealed class MessagePackHttpMethodFormatterTests +{ + [MessagePackObject] + public record HttpMethodDto + { + [Key("method")] + [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] + public HttpMethod? Method { get; set; } + } + + private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + + private static IEnumerable + Fixtures => + new[] + { + new HttpMethodDto + { + Method = null + }, + new HttpMethodDto + { + Method = HttpMethod.Get + }, + new HttpMethodDto + { + Method = new HttpMethod("PROPFIND") + } + }; + + public static IEnumerable SerializeDeserializeFixtures => Fixtures + .Select((x) => new object?[] + { + x, x + }); + + [Theory] + [MemberData(nameof(SerializeDeserializeFixtures))] + public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( + HttpMethodDto input, + HttpMethodDto expected) + { + // arrange + var serialized = MessagePackSerializer.Serialize(input, Options); + + // act + var result = MessagePackSerializer.Deserialize(serialized, Options); + + // assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson("{}", Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().BeEquivalentTo(new HttpMethodDto + { + Method = null + }); + } +} diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs new file mode 100644 index 0000000..151eced --- /dev/null +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -0,0 +1,94 @@ +using MessagePack; +using SocketIO.Core; +using SocketIO.Serializer.MessagePack; + +namespace SecTester.Repeater.Tests.Bus; + +public class IncomingRequestTests +{ + private static readonly IEnumerable ValidFixtures = new[] + { + new IncomingRequest(new Uri("http://foo.bar/1")) + { + Protocol = Protocol.Http, + Method = new HttpMethod("PROPFIND"), + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + }, + Body = "{\"foo\":\"bar\"}" + }, + new IncomingRequest(new Uri("http://foo.bar/2")) + { + Protocol = Protocol.Http, + Headers = new List>>() + } + }; + + public static IEnumerable CreateInstanceFixtures => ValidFixtures + .Select((x) => new object?[] + { + x + }); + + [Theory] + [MemberData(nameof(CreateInstanceFixtures))] + public void IncomingRequest_FromDictionary_ShouldCreateInstance(IncomingRequest input) + { + // arrange + var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + + var serialized = serializer.Serialize(EngineIO.V4, "request", 1, "/some", new object[] { input }).First(); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(serialized.Binary); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var result = IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + result.Should().BeEquivalentTo(input); + } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNotProvided() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"protocol\":0,\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson)); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var act = () => IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + act.Should().Throw(); + } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldAssignDefaultPropertyValues() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson)); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var result = IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1"))); + } +} From 5428396380f34034779c0aa1a98137fc22bda3f5 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 16:28:48 +0400 Subject: [PATCH 05/13] revert(repeater): revert unwanted changes closes #170 --- src/SecTester.Core/Bus/Message.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/SecTester.Core/Bus/Message.cs b/src/SecTester.Core/Bus/Message.cs index 99a187f..efaac6f 100644 --- a/src/SecTester.Core/Bus/Message.cs +++ b/src/SecTester.Core/Bus/Message.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; using SecTester.Core.Utils; namespace SecTester.Core.Bus; @@ -7,9 +6,7 @@ namespace SecTester.Core.Bus; public abstract record Message { public string CorrelationId { get; protected init; } - public DateTime CreatedAt { get; protected init; } - public string Type { get; protected init; } protected Message() From ad15fcbee46625a1e98a43ad3bcf460c953f77f2 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 17:20:48 +0400 Subject: [PATCH 06/13] feat(repeater): improve coverage closes #170 --- src/SecTester.Repeater/Bus/IncomingRequest.cs | 2 +- .../Bus/OutgoingResponse.cs | 2 +- .../MessagePackHttpHeadersFormatterTests.cs | 31 +++++++++++++++++++ .../Bus/IncomingRequestTests.cs | 20 ++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index 3c8913c..b1e6fc6 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -10,7 +10,7 @@ namespace SecTester.Repeater.Bus; [MessagePackObject] -public record IncomingRequest(Uri Url): HttpMessage, IRequest +public record IncomingRequest(Uri Url) : HttpMessage, IRequest { private const string UrlKey = "url"; private const string MethodKey = "method"; diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index a56eca7..266628d 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -4,7 +4,7 @@ namespace SecTester.Repeater.Bus; [MessagePackObject] -public record OutgoingResponse : HttpMessage, IResponse +public record OutgoingResponse : HttpMessage, IResponse { [Key("statusCode")] public int? StatusCode { get; set; } diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs index 23b7b5d..f573764 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs @@ -39,6 +39,22 @@ private static IEnumerable< } }; + public static readonly IEnumerable WrongFormatFixtures = new List + { + new object[] + { + "{\"headers\":5}", + }, + new object[] + { + "{\"headers\":[]}", + }, + new object[] + { + "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}", + } + }; + public static IEnumerable SerializeDeserializeFixtures => Fixtures .Select((x) => new object?[] { @@ -76,4 +92,19 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMis Headers = null }); } + + [Theory] + [MemberData(nameof(WrongFormatFixtures))] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( + string input) + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson(input, Options); + + // act + var act = () => MessagePackSerializer.Deserialize(binary, Options); + + // assert + act.Should().Throw().WithMessage("Failed to deserialize SecTester.Repeater.Tests.Bus.Formatters.MessagePackHttpHeadersFormatterTests+HttpHeadersDto value."); + } } diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index 151eced..0b91bbe 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -91,4 +91,24 @@ public void IncomingRequest_FromDictionary_ShouldAssignDefaultPropertyValues() // assert result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1"))); } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldParseEnumNamedValue() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http\",\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson)); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var result = IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")){ Protocol = Protocol.Http}); + } } From ea763bd17e55b0305089b8d347f0a71c4e9d9757 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 24 May 2024 17:39:32 +0400 Subject: [PATCH 07/13] feat(repeater): fix formatting, improve coverage closes #170 --- .../MessagePackHttpHeadersFormatterTests.cs | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs index f573764..248f145 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs @@ -15,46 +15,36 @@ public record HttpHeadersDto private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - private static IEnumerable< - HttpHeadersDto> - Fixtures => - new[] - { - new HttpHeadersDto - { - Headers = null - }, - new HttpHeadersDto - { - Headers = new List>>() - }, - - new HttpHeadersDto - { - Headers = new List>> - { - new("content-type", new List { "application/json" }), - new("cache-control", new List { "no-cache", "no-store" }) - } - } - }; - - public static readonly IEnumerable WrongFormatFixtures = new List + private static IEnumerable Fixtures => new[] { - new object[] + new HttpHeadersDto { - "{\"headers\":5}", + Headers = null }, - new object[] + new HttpHeadersDto { - "{\"headers\":[]}", + Headers = new List>>() }, - new object[] + new HttpHeadersDto { - "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}", + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + } } }; + public static readonly IEnumerable WrongFormatFixtures = new[] + { + "{\"headers\":5}", + "{\"headers\":[]}", + "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}", + "{\"headers\":{\"content-type\":1}}", + "{\"headers\":{\"content-type\":[null]}}", + "{\"headers\":{\"content-type\":[1]}}" + }; + public static IEnumerable SerializeDeserializeFixtures => Fixtures .Select((x) => new object?[] { @@ -93,8 +83,14 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMis }); } + public static IEnumerable ThrowWhenWrongFormatFixtures => WrongFormatFixtures + .Select((x) => new object?[] + { + x + }); + [Theory] - [MemberData(nameof(WrongFormatFixtures))] + [MemberData(nameof(ThrowWhenWrongFormatFixtures))] public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( string input) { @@ -105,6 +101,7 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( var act = () => MessagePackSerializer.Deserialize(binary, Options); // assert - act.Should().Throw().WithMessage("Failed to deserialize SecTester.Repeater.Tests.Bus.Formatters.MessagePackHttpHeadersFormatterTests+HttpHeadersDto value."); + act.Should().Throw().WithMessage( + "Failed to deserialize SecTester.Repeater.Tests.Bus.Formatters.MessagePackHttpHeadersFormatterTests+HttpHeadersDto value."); } } From 82502d21c5ea4bbdecd458481bd75488cecd4e10 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Mon, 27 May 2024 14:19:52 +0400 Subject: [PATCH 08/13] feat(repeater): support stringified enum value serialization closes #170 --- .../MessagePackStringEnumFormatter.cs | 50 ++++++++++++ src/SecTester.Repeater/Bus/IncomingRequest.cs | 13 +++- src/SecTester.Repeater/Protocol.cs | 3 + .../MessagePackHttpHeadersFormatterTests.cs | 69 +++++++++-------- .../MessagePackHttpMethodFormatterTests.cs | 32 ++++---- .../MessagePackStringEnumFormatterTests.cs | 76 +++++++++++++++++++ .../Bus/IncomingRequestTests.cs | 2 +- 7 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs create mode 100644 test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs new file mode 100644 index 0000000..76f9c94 --- /dev/null +++ b/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Bus.Formatters; + +// ADHOC: MessagePack-CSharp prohibits declaration of IMessagePackFormatter requesting to use System.Enum instead, refer to formatter interface argument type check +// https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/db2320b3338735c9266110bbbfffe63f17dfdf46/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Resolvers/DynamicObjectResolver.cs#L623 + +public class MessagePackStringEnumFormatter : IMessagePackFormatter + where T : Enum +{ + private static readonly Dictionary EnumToString = typeof(T) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(field => new + { + Value = (T)field.GetValue(null), + StringValue = field.GetCustomAttribute()?.Value ?? field.Name + }) + .ToDictionary(x => x.Value, x => x.StringValue); + + private static readonly Dictionary StringToEnum = EnumToString.ToDictionary(x => x.Value, x => x.Key); + + public void Serialize(ref MessagePackWriter writer, Enum value, MessagePackSerializerOptions options) + { + if (!EnumToString.TryGetValue((T)value, out var stringValue)) + { + throw new MessagePackSerializationException($"No string representation found for {value}"); + } + + writer.Write(stringValue); + } + + public Enum Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + + var stringValue = reader.ReadString(); + + if (!StringToEnum.TryGetValue(stringValue, out var enumValue)) + { + throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); + } + + return enumValue; + } +} diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index b1e6fc6..1117e34 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Reflection; +using System.Runtime.Serialization; using MessagePack; using SecTester.Repeater.Bus.Formatters; using SecTester.Repeater.Runners; @@ -15,6 +17,15 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest private const string UrlKey = "url"; private const string MethodKey = "method"; + private static readonly Dictionary ProtocolEntries = typeof(Protocol) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(field => new + { + Value = (Protocol)field.GetValue(null), + StringValue = field.GetCustomAttribute()?.Value ?? field.Name + }) + .ToDictionary(x => x.StringValue, x => x.Value); + [Key(MethodKey)] [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] public HttpMethod Method { get; set; } = HttpMethod.Get; @@ -24,7 +35,7 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest public static IncomingRequest FromDictionary(Dictionary dictionary) { - var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && Enum.TryParse(p.ToString(), out var e) + var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && ProtocolEntries.TryGetValue(p.ToString(), out var e) ? e : Protocol.Http; diff --git a/src/SecTester.Repeater/Protocol.cs b/src/SecTester.Repeater/Protocol.cs index 4221d78..f35c880 100644 --- a/src/SecTester.Repeater/Protocol.cs +++ b/src/SecTester.Repeater/Protocol.cs @@ -1,6 +1,9 @@ +using System.Runtime.Serialization; + namespace SecTester.Repeater; public enum Protocol { + [EnumMember(Value = "http")] Http } diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs index 248f145..8be0cb1 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs @@ -15,47 +15,52 @@ public record HttpHeadersDto private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - private static IEnumerable Fixtures => new[] + public static readonly IEnumerable Fixtures = new List() { - new HttpHeadersDto + new object[] { - Headers = null + new HttpHeadersDto + { + Headers = null + } }, - new HttpHeadersDto + new object[] { - Headers = new List>>() + new HttpHeadersDto + { + Headers = new List>>() + } }, - new HttpHeadersDto + new object[] { - Headers = new List>> + new HttpHeadersDto { - new("content-type", new List { "application/json" }), - new("cache-control", new List { "no-cache", "no-store" }) + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + } } } }; - public static readonly IEnumerable WrongFormatFixtures = new[] + public static readonly IEnumerable WrongValueFixtures = new List() { - "{\"headers\":5}", - "{\"headers\":[]}", - "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}", - "{\"headers\":{\"content-type\":1}}", - "{\"headers\":{\"content-type\":[null]}}", - "{\"headers\":{\"content-type\":[1]}}" - }; - - public static IEnumerable SerializeDeserializeFixtures => Fixtures - .Select((x) => new object?[] + new object[] { - x, x - }); + "{\"headers\":5}" + }, + new object[] { "{\"headers\":[]}" }, + new object[] { "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}" }, + new object[] { "{\"headers\":{\"content-type\":1}}" }, + new object[] { "{\"headers\":{\"content-type\":[null]}}" }, + new object[] { "{\"headers\":{\"content-type\":[1]}}" } + }; [Theory] - [MemberData(nameof(SerializeDeserializeFixtures))] + [MemberData(nameof(Fixtures))] public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - HttpHeadersDto input, - HttpHeadersDto expected) + HttpHeadersDto input) { // arrange var serialized = MessagePackSerializer.Serialize(input, Options); @@ -64,7 +69,7 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeseriali var result = MessagePackSerializer.Deserialize(serialized, Options); // assert - result.Should().BeEquivalentTo(expected); + result.Should().BeEquivalentTo(input); } [Fact] @@ -83,15 +88,9 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMis }); } - public static IEnumerable ThrowWhenWrongFormatFixtures => WrongFormatFixtures - .Select((x) => new object?[] - { - x - }); - [Theory] - [MemberData(nameof(ThrowWhenWrongFormatFixtures))] - public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( + [MemberData(nameof(WrongValueFixtures))] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue( string input) { // arrange @@ -102,6 +101,6 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( // assert act.Should().Throw().WithMessage( - "Failed to deserialize SecTester.Repeater.Tests.Bus.Formatters.MessagePackHttpHeadersFormatterTests+HttpHeadersDto value."); + "Failed to deserialize*"); } } diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs index b4d30f9..356fcea 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs @@ -15,35 +15,35 @@ public record HttpMethodDto private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - private static IEnumerable - Fixtures => - new[] + public static readonly IEnumerable Fixtures = new List() + { + new object[] { new HttpMethodDto { Method = null - }, + } + }, + new object[] + { new HttpMethodDto { Method = HttpMethod.Get - }, + } + }, + new object[] + { new HttpMethodDto { Method = new HttpMethod("PROPFIND") } - }; - - public static IEnumerable SerializeDeserializeFixtures => Fixtures - .Select((x) => new object?[] - { - x, x - }); + } + }; [Theory] - [MemberData(nameof(SerializeDeserializeFixtures))] + [MemberData(nameof(Fixtures))] public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - HttpMethodDto input, - HttpMethodDto expected) + HttpMethodDto input) { // arrange var serialized = MessagePackSerializer.Serialize(input, Options); @@ -52,7 +52,7 @@ public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializ var result = MessagePackSerializer.Deserialize(serialized, Options); // assert - result.Should().BeEquivalentTo(expected); + result.Should().BeEquivalentTo(input); } [Fact] diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs new file mode 100644 index 0000000..ce44db5 --- /dev/null +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs @@ -0,0 +1,76 @@ +using System.Runtime.Serialization; +using MessagePack; +using SecTester.Repeater.Bus.Formatters; + +namespace SecTester.Repeater.Tests.Bus.Formatters; + +public sealed class MessagePackStringEnumFormatterTests +{ + public enum Foo + { + [EnumMember(Value = "bar")] + Bar = 0, + [EnumMember(Value = "baz_cux")] + BazCux = 1 + } + + [MessagePackObject] + public record EnumDto + { + [Key("foo")] + [MessagePackFormatter(typeof(MessagePackStringEnumFormatter))] + public Enum Foo { get; set; } + } + + private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + + public static IEnumerable + Fixtures => + new List + { + new object[] { "{\"foo\":\"bar\"}" }, + new object[] { "{\"foo\":\"baz_cux\"}" } + }; + + public static IEnumerable + WrongValueFixtures => + new List + { + new object[] { "{\"foo\": null}" }, + new object[] { "{\"foo\": 5}" }, + new object[] { "{\"foo\":\"BazCux\"}" } + }; + + + [Theory] + [MemberData(nameof(Fixtures))] + public void MessagePackStringEnumFormatter_Serialize_ShouldCorrectlySerialize( + string input) + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson(input, Options); + var obj = MessagePackSerializer.Deserialize(binary, Options); + + + // act + var result = MessagePackSerializer.SerializeToJson(obj, Options); + + // assert + result.Should().BeEquivalentTo(input); + } + + [Theory] + [MemberData(nameof(WrongValueFixtures))] + public void MessagePackStringEnumFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(string input) + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson(input, Options); + + // act + var act = () => MessagePackSerializer.Deserialize(binary, Options); + + // assert + act.Should().Throw().WithMessage( + "Failed to deserialize*"); + } +} diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index 0b91bbe..69e889f 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -57,7 +57,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot { // arrange var packJson = - "{\"type\":2,\"data\":[\"request\",{\"protocol\":0,\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http:\",\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); From 57e168a4cc362b9795ec0953a7c5f78ca965d2aa Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 31 May 2024 14:51:45 +0400 Subject: [PATCH 09/13] feat(repeater): address pr comments closes #170 --- .../MessagePackHttpHeadersFormatter.cs | 161 ------------------ .../MessagePackHttpMethodFormatter.cs | 36 ---- .../MessagePackStringEnumFormatter.cs | 50 ------ src/SecTester.Repeater/Bus/HttpMessage.cs | 23 --- src/SecTester.Repeater/Bus/IncomingRequest.cs | 19 ++- .../Bus/OutgoingResponse.cs | 15 +- src/SecTester.Repeater/Bus/SocketIoMessage.cs | 12 +- .../Bus/SocketIoRepeaterBus.cs | 3 +- .../DefaultMessagePackSerializerOptions.cs | 18 ++ .../MessagePackHttpHeadersFormatter.cs | 155 +++++++++++++++++ .../MessagePackHttpMethodFormatter.cs | 74 ++++++++ .../Internal/MessagePackNamingPolicy.cs | 8 + .../MessagePackSnakeCaseNamingPolicy.cs | 11 ++ .../MessagePackStringEnumMemberFormatter.cs | 53 ++++++ .../SecTester.Repeater.csproj | 2 +- .../MessagePackHttpMethodFormatterTests.cs | 73 -------- .../MessagePackStringEnumFormatterTests.cs | 76 --------- ...efaultMessagePackSerializerOptionsTests.cs | 82 +++++++++ .../MessagePackHttpHeadersFormatterTests.cs | 43 ++--- .../MessagePackHttpMethodFormatterTests.cs | 63 +++++++ ...ssagePackStringEnumMemberFormatterTests.cs | 75 ++++++++ 21 files changed, 591 insertions(+), 461 deletions(-) delete mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs delete mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs delete mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs delete mode 100644 src/SecTester.Repeater/Bus/HttpMessage.cs create mode 100644 src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs delete mode 100644 test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs delete mode 100644 test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs create mode 100644 test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs rename test/SecTester.Repeater.Tests/{Bus/Formatters => Internal}/MessagePackHttpHeadersFormatterTests.cs (73%) create mode 100644 test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs create mode 100644 test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs deleted file mode 100644 index 5e835a9..0000000 --- a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpHeadersFormatter.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using MessagePack; -using MessagePack.Formatters; - -namespace SecTester.Repeater.Bus.Formatters; - -// Headers formatter is to be support javascript `undefined` which is treated as null (0xC0) -// https://www.npmjs.com/package/@msgpack/msgpack#messagepack-mapping-table -// https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format - -public class MessagePackHttpHeadersFormatter : IMessagePackFormatter< - IEnumerable>>? -> -{ - public void Serialize(ref MessagePackWriter writer, IEnumerable>>? value, - MessagePackSerializerOptions options) - { - if (value == null) - { - writer.WriteNil(); - } - else - { - var count = value.Count(); - - writer.WriteMapHeader(count); - - SerializeMap(ref writer, value); - } - } - - public IEnumerable>>? Deserialize(ref MessagePackReader reader, - MessagePackSerializerOptions options) - { - switch (reader.NextMessagePackType) - { - case MessagePackType.Nil: - return null; - case MessagePackType.Map: - break; - default: - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, - "Unrecognized code: 0x{0:X2} but expected to be a map or null", reader.NextCode)); - } - - var length = reader.ReadMapHeader(); - - options.Security.DepthStep(ref reader); - - try - { - return DeserializeMap(ref reader, length, options); - } - finally - { - reader.Depth--; - } - } - - private static void SerializeMap(ref MessagePackWriter writer, IEnumerable>> value) - { - foreach (var item in value) - { - writer.Write(item.Key); - - SerializeValue(ref writer, item); - } - } - - private static void SerializeValue(ref MessagePackWriter writer, KeyValuePair> item) - { - var headersCount = item.Value.Count(); - - if (headersCount == 1) - { - writer.Write(item.Value.First()); - } - else - { - writer.WriteArrayHeader(headersCount); - - foreach (var subItem in item.Value) - { - writer.Write(subItem); - } - } - } - - private static List>> DeserializeMap(ref MessagePackReader reader, int length, - MessagePackSerializerOptions options) - { - var result = new List>>(length); - - for (var i = 0 ; i < length ; i++) - { - var key = DeserializeString(ref reader); - - switch (reader.NextMessagePackType) - { - case MessagePackType.String: - result.Add(new KeyValuePair>(key, new List { DeserializeString(ref reader) })); - break; - case MessagePackType.Array: - result.Add(new KeyValuePair>(key, DeserializeArray(ref reader, reader.ReadArrayHeader(), options))); - break; - default: - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, - "Unrecognized code: 0x{0:X2} but expected to be either a string or an array.", reader.NextCode)); - } - } - - - return result; - } - - private static IEnumerable DeserializeArray(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) - { - var result = new List(length); - - if (length == 0) - { - return result; - } - - options.Security.DepthStep(ref reader); - - try - { - for (var i = 0 ; i < length ; i++) - { - result.Add(DeserializeString(ref reader)); - } - } - finally - { - reader.Depth--; - } - - return result; - } - - private static string DeserializeString(ref MessagePackReader reader) - { - if (reader.NextMessagePackType != MessagePackType.String) - { - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, - "Unrecognized code: 0x{0:X2} but expected to be a string.", reader.NextCode)); - } - - var value = reader.ReadString(); - - if (null == value) - { - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Nulls are not allowed.")); - } - - return value; - } -} diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs deleted file mode 100644 index 12cb5eb..0000000 --- a/src/SecTester.Repeater/Bus/Formatters/MessagePackHttpMethodFormatter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Globalization; -using System.Net.Http; -using MessagePack; -using MessagePack.Formatters; - -namespace SecTester.Repeater.Bus.Formatters; - -public class MessagePackHttpMethodFormatter : IMessagePackFormatter -{ - public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePackSerializerOptions options) - { - if (null == value) - { - writer.WriteNil(); - } - else - { - writer.Write(value.Method); - } - } - - public HttpMethod? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) - { - switch (reader.NextMessagePackType) - { - case MessagePackType.Nil: - return null; - case MessagePackType.String: - var method = reader.ReadString(); - return string.IsNullOrWhiteSpace(method) ? null : new HttpMethod(method); - default: - throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, - "Unrecognized code: 0x{0:X2} but expected to be either a string or null.", reader.NextCode)); - } - } -} diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs deleted file mode 100644 index 76f9c94..0000000 --- a/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.Serialization; -using MessagePack; -using MessagePack.Formatters; - -namespace SecTester.Repeater.Bus.Formatters; - -// ADHOC: MessagePack-CSharp prohibits declaration of IMessagePackFormatter requesting to use System.Enum instead, refer to formatter interface argument type check -// https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/db2320b3338735c9266110bbbfffe63f17dfdf46/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Resolvers/DynamicObjectResolver.cs#L623 - -public class MessagePackStringEnumFormatter : IMessagePackFormatter - where T : Enum -{ - private static readonly Dictionary EnumToString = typeof(T) - .GetFields(BindingFlags.Public | BindingFlags.Static) - .Select(field => new - { - Value = (T)field.GetValue(null), - StringValue = field.GetCustomAttribute()?.Value ?? field.Name - }) - .ToDictionary(x => x.Value, x => x.StringValue); - - private static readonly Dictionary StringToEnum = EnumToString.ToDictionary(x => x.Value, x => x.Key); - - public void Serialize(ref MessagePackWriter writer, Enum value, MessagePackSerializerOptions options) - { - if (!EnumToString.TryGetValue((T)value, out var stringValue)) - { - throw new MessagePackSerializationException($"No string representation found for {value}"); - } - - writer.Write(stringValue); - } - - public Enum Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) - { - - var stringValue = reader.ReadString(); - - if (!StringToEnum.TryGetValue(stringValue, out var enumValue)) - { - throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); - } - - return enumValue; - } -} diff --git a/src/SecTester.Repeater/Bus/HttpMessage.cs b/src/SecTester.Repeater/Bus/HttpMessage.cs deleted file mode 100644 index 05fb3ae..0000000 --- a/src/SecTester.Repeater/Bus/HttpMessage.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using MessagePack; -using SecTester.Repeater.Bus.Formatters; - -namespace SecTester.Repeater.Bus; - -public abstract record HttpMessage -{ - public const string HeadersKey = "headers"; - public const string BodyKey = "body"; - public const string ProtocolKey = "protocol"; - - [Key(ProtocolKey)] - public Protocol Protocol { get; set; } = Protocol.Http; - - [Key(HeadersKey)] - [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] - public IEnumerable>> Headers { get; set; } = - new List>>(); - - [Key(BodyKey)] - public string? Body { get; set; } -} diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index 1117e34..017a6e5 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -6,16 +6,19 @@ using System.Reflection; using System.Runtime.Serialization; using MessagePack; -using SecTester.Repeater.Bus.Formatters; +using SecTester.Repeater.Internal; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; [MessagePackObject] -public record IncomingRequest(Uri Url) : HttpMessage, IRequest +public record IncomingRequest(Uri Url) : IRequest { private const string UrlKey = "url"; private const string MethodKey = "method"; + private const string HeadersKey = "headers"; + private const string BodyKey = "body"; + private const string ProtocolKey = "protocol"; private static readonly Dictionary ProtocolEntries = typeof(Protocol) .GetFields(BindingFlags.Public | BindingFlags.Static) @@ -26,8 +29,18 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest }) .ToDictionary(x => x.StringValue, x => x.Value); + + [Key(ProtocolKey)] + public Protocol Protocol { get; set; } = Protocol.Http; + + [Key(HeadersKey)] + public IEnumerable>> Headers { get; set; } = + new List>>(); + + [Key(BodyKey)] + public string? Body { get; set; } + [Key(MethodKey)] - [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] public HttpMethod Method { get; set; } = HttpMethod.Get; [Key(UrlKey)] diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index 266628d..f21f6ee 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -1,17 +1,30 @@ +using System.Collections.Generic; using MessagePack; +using SecTester.Repeater.Internal; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; [MessagePackObject] -public record OutgoingResponse : HttpMessage, IResponse +public record OutgoingResponse : IResponse { + [Key("protocol")] + public Protocol Protocol { get; set; } = Protocol.Http; + [Key("statusCode")] public int? StatusCode { get; set; } + [Key("body")] + public string? Body { get; set; } + [Key("message")] public string? Message { get; set; } [Key("errorCode")] public string? ErrorCode { get; set; } + + [Key("headers")] + public IEnumerable>> Headers { get; set; } = + new List>>(); + } diff --git a/src/SecTester.Repeater/Bus/SocketIoMessage.cs b/src/SecTester.Repeater/Bus/SocketIoMessage.cs index 839825e..12c6e43 100644 --- a/src/SecTester.Repeater/Bus/SocketIoMessage.cs +++ b/src/SecTester.Repeater/Bus/SocketIoMessage.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SecTester.Repeater.Bus.Formatters; +using SecTester.Repeater.Internal; using SocketIOClient; namespace SecTester.Repeater.Bus; @@ -16,15 +16,7 @@ public SocketIoMessage(SocketIOResponse response) _response = response ?? throw new ArgumentNullException(nameof(response)); } - public virtual T GetValue(int index = 0) - { - if (typeof(T) == typeof(IncomingRequest)) - { - return (T)(object)IncomingRequest.FromDictionary(_response.GetValue>(index)); - } - - return _response.GetValue(index); - } + public virtual T GetValue(int index = 0) => _response.GetValue(index); public virtual Task CallbackAsync(params object[] data) => _response.CallbackAsync(data); diff --git a/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs b/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs index fa3fd44..db5e7fd 100644 --- a/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs +++ b/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Timers; @@ -64,7 +65,7 @@ private void DelegateEvents() } var ct = new CancellationTokenSource(_options.AckTimeout); - var request = response.GetValue(); + var request = IncomingRequest.FromDictionary(response.GetValue>()); var result = await RequestReceived.Invoke(request).ConfigureAwait(false); await response.CallbackAsync(ct.Token, result).ConfigureAwait(false); }); diff --git a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs new file mode 100644 index 0000000..16755a9 --- /dev/null +++ b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs @@ -0,0 +1,18 @@ +using MessagePack; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Internal; + +internal class DefaultMessagePackSerializerOptions +{ + internal static readonly MessagePackSerializerOptions Instance = new( + CompositeResolver.Create( + CompositeResolver.Create( + new MessagePackHttpHeadersFormatter(), + new MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy.SnakeCase), + new MessagePackHttpMethodFormatter()), + StandardResolver.Instance + ) + ); +} diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs new file mode 100644 index 0000000..1c9edea --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using System.Linq; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Internal; + +// Headers formatter is to be supporting javascript `undefined` which is treated as null (0xC0) +// https://www.npmjs.com/package/@msgpack/msgpack#messagepack-mapping-table +// https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format + +internal class MessagePackHttpHeadersFormatter : IMessagePackFormatter< + List>>? +> +{ + public void Serialize(ref MessagePackWriter writer, List>>? value, + MessagePackSerializerOptions options) + { + if (value == null) + { + writer.WriteNil(); + } + else + { + var count = value.Count(); + + writer.WriteMapHeader(count); + + Serialize(ref writer, value); + } + } + + private static void Serialize(ref MessagePackWriter writer, IEnumerable>> value) + { + foreach (var item in value) + { + writer.Write(item.Key); + + Serialize(ref writer, item); + } + } + + private static void Serialize(ref MessagePackWriter writer, KeyValuePair> item) + { + var headersCount = item.Value.Count(); + + if (headersCount == 1) + { + writer.Write(item.Value.First()); + } + else + { + writer.WriteArrayHeader(headersCount); + + foreach (var subItem in item.Value) + { + writer.Write(subItem); + } + } + } + + public List>>? Deserialize(ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + if (reader.NextMessagePackType == MessagePackType.Nil) + { + reader.ReadNil(); + return null; + } + + if (reader.NextMessagePackType != MessagePackType.Map) + { + throw new MessagePackSerializationException($"Unrecognized code: 0x{reader.NextCode:X2} but expected to be a map or null"); + } + + var length = reader.ReadMapHeader(); + + options.Security.DepthStep(ref reader); + + try + { + return DeserializeMap(ref reader, length, options); + } + finally + { + reader.Depth--; + } + } + + private static List>> DeserializeMap(ref MessagePackReader reader, int length, + MessagePackSerializerOptions options) + { + var result = new List>>(length); + + for (var i = 0 ; i < length ; i++) + { + var key = DeserializeString(ref reader); + + result.Add(new KeyValuePair>( + key, + DeserializeValue(ref reader, options) + )); + } + + return result; + } + + private static IEnumerable DeserializeArray(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) + { + var result = new List(length); + + options.Security.DepthStep(ref reader); + + try + { + for (var i = 0 ; i < length ; i++) + { + result.Add(DeserializeString(ref reader)); + } + } + finally + { + reader.Depth--; + } + + return result; + } + + private static IEnumerable DeserializeValue(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + switch (reader.NextMessagePackType) + { + case MessagePackType.Nil: + reader.ReadNil(); + return new List(); + case MessagePackType.String: + return new List { DeserializeString(ref reader) }; + case MessagePackType.Array: + return DeserializeArray(ref reader, reader.ReadArrayHeader(), options); + default: + throw new MessagePackSerializationException( + $"Unrecognized code: 0x{reader.NextCode:X2} but expected to be either a string or an array."); + } + } + + private static string DeserializeString(ref MessagePackReader reader) + { + if (reader.NextMessagePackType != MessagePackType.String) + { + throw new MessagePackSerializationException($"Unrecognized code: 0x{reader.NextCode:X2} but expected to be a string."); + } + + return reader.ReadString() ?? string.Empty; + } +} diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs new file mode 100644 index 0000000..d362a64 --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Internal; + +internal class MessagePackHttpMethodFormatter : IMessagePackFormatter +{ + private static readonly IEnumerable BaseMethods = typeof(HttpMethod) + .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(x => x.PropertyType.IsAssignableFrom(typeof(HttpMethod))) + .Select(x => x.GetValue(null)) + .Cast(); + + private static readonly IEnumerable CustomMethods = new List + { + new("PATCH"), + new("COPY"), + new("LINK"), + new("UNLINK"), + new("PURGE"), + new("LOCK"), + new("UNLOCK"), + new("PROPFIND"), + new("VIEW") + }; + + private static readonly IDictionary Methods = BaseMethods.Concat(CustomMethods).Distinct() + .ToDictionary(x => x.Method, x => x, StringComparer.InvariantCultureIgnoreCase); + public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePackSerializerOptions options) + { + if (null == value) + { + writer.WriteNil(); + } + else + { + writer.Write(value.Method); + } + } + + public HttpMethod? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.NextMessagePackType == MessagePackType.Nil) + { + reader.ReadNil(); + return null; + } + + if (reader.NextMessagePackType != MessagePackType.String) + { + throw new MessagePackSerializationException($"Unrecognized code: 0x{reader.NextCode:X2} but expected to be either a string or null."); + } + + return Deserialize(ref reader); + } + + private static HttpMethod? Deserialize(ref MessagePackReader reader) + { + var token = reader.ReadString(); + + if (token is null || !Methods.TryGetValue(token, out var method)) + { + throw new MessagePackSerializationException( + $"Unexpected value {token} when parsing the {nameof(HttpMethod)}."); + } + + return method; + } +} diff --git a/src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs b/src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs new file mode 100644 index 0000000..f7bdfa8 --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs @@ -0,0 +1,8 @@ +namespace SecTester.Repeater.Internal; + +internal abstract class MessagePackNamingPolicy +{ + public static MessagePackNamingPolicy SnakeCase { get; } = new MessagePackSnakeCaseNamingPolicy(); + + public abstract string ConvertName(string name); +} diff --git a/src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs b/src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs new file mode 100644 index 0000000..1effd1b --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs @@ -0,0 +1,11 @@ +using SecTester.Core.Utils; + +namespace SecTester.Repeater.Internal; + +internal class MessagePackSnakeCaseNamingPolicy : MessagePackNamingPolicy +{ + public override string ConvertName(string name) + { + return name.ToSnakeCase(); + } +} diff --git a/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs new file mode 100644 index 0000000..1db48fa --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Internal; + +internal class MessagePackStringEnumMemberFormatter : IMessagePackFormatter + where T : Enum +{ + private static readonly Dictionary EnumToString = typeof(T) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(field => new + { + Value = (T)field.GetValue(null), + StringValue = field.GetCustomAttribute()?.Value ?? field.Name + }) + .ToDictionary(x => x.Value, x => x.StringValue); + + private readonly Dictionary CasedStringToEnum; + private readonly Dictionary CasedEnumToString; + + public MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy namingPolicy) + { + this.CasedEnumToString = EnumToString.ToDictionary(x => x.Key, x => namingPolicy.ConvertName(x.Value)); + this.CasedStringToEnum = EnumToString.ToDictionary(x => namingPolicy.ConvertName(x.Value), x => x.Key); + } + + public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options) + { + if (!CasedEnumToString.TryGetValue(value, out var stringValue)) + { + throw new MessagePackSerializationException($"No string representation found for {value}"); + } + + writer.Write(stringValue); + } + + public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + var stringValue = reader.ReadString(); + + if (null == stringValue || !CasedStringToEnum.TryGetValue(stringValue, out var enumValue)) + { + throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); + } + + return enumValue; + } +} diff --git a/src/SecTester.Repeater/SecTester.Repeater.csproj b/src/SecTester.Repeater/SecTester.Repeater.csproj index 8468a26..8f6038b 100644 --- a/src/SecTester.Repeater/SecTester.Repeater.csproj +++ b/src/SecTester.Repeater/SecTester.Repeater.csproj @@ -17,8 +17,8 @@ + - diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs deleted file mode 100644 index 356fcea..0000000 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using MessagePack; -using SecTester.Repeater.Bus.Formatters; - -namespace SecTester.Repeater.Tests.Bus.Formatters; - -public sealed class MessagePackHttpMethodFormatterTests -{ - [MessagePackObject] - public record HttpMethodDto - { - [Key("method")] - [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] - public HttpMethod? Method { get; set; } - } - - private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - - public static readonly IEnumerable Fixtures = new List() - { - new object[] - { - new HttpMethodDto - { - Method = null - } - }, - new object[] - { - new HttpMethodDto - { - Method = HttpMethod.Get - } - }, - new object[] - { - new HttpMethodDto - { - Method = new HttpMethod("PROPFIND") - } - } - }; - - [Theory] - [MemberData(nameof(Fixtures))] - public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - HttpMethodDto input) - { - // arrange - var serialized = MessagePackSerializer.Serialize(input, Options); - - // act - var result = MessagePackSerializer.Deserialize(serialized, Options); - - // assert - result.Should().BeEquivalentTo(input); - } - - [Fact] - public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() - { - // arrange - var binary = MessagePackSerializer.ConvertFromJson("{}", Options); - - // act - var result = MessagePackSerializer.Deserialize(binary, Options); - - // assert - result.Should().BeEquivalentTo(new HttpMethodDto - { - Method = null - }); - } -} diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs deleted file mode 100644 index ce44db5..0000000 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Runtime.Serialization; -using MessagePack; -using SecTester.Repeater.Bus.Formatters; - -namespace SecTester.Repeater.Tests.Bus.Formatters; - -public sealed class MessagePackStringEnumFormatterTests -{ - public enum Foo - { - [EnumMember(Value = "bar")] - Bar = 0, - [EnumMember(Value = "baz_cux")] - BazCux = 1 - } - - [MessagePackObject] - public record EnumDto - { - [Key("foo")] - [MessagePackFormatter(typeof(MessagePackStringEnumFormatter))] - public Enum Foo { get; set; } - } - - private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - - public static IEnumerable - Fixtures => - new List - { - new object[] { "{\"foo\":\"bar\"}" }, - new object[] { "{\"foo\":\"baz_cux\"}" } - }; - - public static IEnumerable - WrongValueFixtures => - new List - { - new object[] { "{\"foo\": null}" }, - new object[] { "{\"foo\": 5}" }, - new object[] { "{\"foo\":\"BazCux\"}" } - }; - - - [Theory] - [MemberData(nameof(Fixtures))] - public void MessagePackStringEnumFormatter_Serialize_ShouldCorrectlySerialize( - string input) - { - // arrange - var binary = MessagePackSerializer.ConvertFromJson(input, Options); - var obj = MessagePackSerializer.Deserialize(binary, Options); - - - // act - var result = MessagePackSerializer.SerializeToJson(obj, Options); - - // assert - result.Should().BeEquivalentTo(input); - } - - [Theory] - [MemberData(nameof(WrongValueFixtures))] - public void MessagePackStringEnumFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(string input) - { - // arrange - var binary = MessagePackSerializer.ConvertFromJson(input, Options); - - // act - var act = () => MessagePackSerializer.Deserialize(binary, Options); - - // assert - act.Should().Throw().WithMessage( - "Failed to deserialize*"); - } -} diff --git a/test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs b/test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs new file mode 100644 index 0000000..b584537 --- /dev/null +++ b/test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs @@ -0,0 +1,82 @@ +using MessagePack; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Tests.Internal; + +public sealed class DefaultMessagePackSerializerOptionsTests +{ + [MessagePackObject] + public record TestDto + { + [Key("protocol")] + public Protocol Protocol { get; set; } = Protocol.Http; + + [Key("method")] + public HttpMethod Method { get; set; } = HttpMethod.Delete; + + private IEnumerable>>? _headers; + + [Key("headers")] + public IEnumerable>>? Headers { + get => this._headers; + set => this._headers = null != value ? new List>>(value) : value; + } + } + + private static readonly MessagePackSerializerOptions Options = DefaultMessagePackSerializerOptions.Instance; + + private static IEnumerable< + TestDto> + Fixtures => + new [] + { + new TestDto + { + Protocol = Protocol.Http, + Method = HttpMethod.Put, + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + } + } + }; + + public static IEnumerable SerializeDeserializeFixtures => Fixtures + .Select((x) => new object?[] + { + x, x + }); + + [Theory] + [MemberData(nameof(SerializeDeserializeFixtures))] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( + TestDto input, + TestDto expected) + { + // arrange + var serialized = MessagePackSerializer.Serialize(input, Options); + + // act + var result = MessagePackSerializer.Deserialize(serialized, Options); + + // assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson("{}", Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().BeEquivalentTo(new TestDto + { + Headers = null + }); + } +} diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs similarity index 73% rename from test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs rename to test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs index 8be0cb1..12bc2c8 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs @@ -1,46 +1,36 @@ using MessagePack; -using SecTester.Repeater.Bus.Formatters; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; -namespace SecTester.Repeater.Tests.Bus.Formatters; +namespace SecTester.Repeater.Tests.Internal; public sealed class MessagePackHttpHeadersFormatterTests { - [MessagePackObject] - public record HttpHeadersDto - { - [Key("headers")] - [MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))] - public IEnumerable>>? Headers { get; set; } - } - - private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create(new MessagePackHttpHeadersFormatter()), + BuiltinResolver.Instance + ) + ); public static readonly IEnumerable Fixtures = new List() { new object[] { - new HttpHeadersDto - { - Headers = null - } + null }, new object[] { - new HttpHeadersDto - { - Headers = new List>>() - } + new List>>() + }, new object[] { - new HttpHeadersDto - { - Headers = new List>> + new List>> { new("content-type", new List { "application/json" }), new("cache-control", new List { "no-cache", "no-store" }) } - } } }; @@ -60,18 +50,18 @@ public record HttpHeadersDto [Theory] [MemberData(nameof(Fixtures))] public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - HttpHeadersDto input) + List>>? input) { // arrange var serialized = MessagePackSerializer.Serialize(input, Options); // act - var result = MessagePackSerializer.Deserialize(serialized, Options); + var result = MessagePackSerializer.Deserialize>>>(serialized, Options); // assert result.Should().BeEquivalentTo(input); } - +/* [Fact] public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() { @@ -103,4 +93,5 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrowWhenDataHasWr act.Should().Throw().WithMessage( "Failed to deserialize*"); } + */ } diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs new file mode 100644 index 0000000..13bd7df --- /dev/null +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs @@ -0,0 +1,63 @@ +using MessagePack; +using MessagePack.Formatters; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Tests.Internal; + +public sealed class MessagePackHttpMethodFormatterTests +{ + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create(new MessagePackHttpMethodFormatter()), + BuiltinResolver.Instance + ) + ); + + public static readonly IEnumerable Fixture = new List + { + new object[] { "DELETE", HttpMethod.Delete }, + new object[] { "GET", HttpMethod.Get }, + new object[] { "HEAD", HttpMethod.Head }, + new object[] { "OPTIONS", HttpMethod.Options }, + new object[] { "PATCH", HttpMethod.Patch }, + new object[] { "POST", HttpMethod.Post }, + new object[] { "PUT", HttpMethod.Put }, + new object[] { "TRACE", HttpMethod.Trace }, + new object[] { "COPY", new HttpMethod("COPY") }, + new object[] { "LINK", new HttpMethod("LINK") }, + new object[] { "UNLINK", new HttpMethod("UNLINK") }, + new object[] { "PURGE", new HttpMethod("PURGE") }, + new object[] { "LOCK", new HttpMethod("LOCK") }, + new object[] { "UNLOCK", new HttpMethod("UNLOCK") }, + new object[] { "PROPFIND", new HttpMethod("PROPFIND") }, + new object[] { "VIEW", new HttpMethod("VIEW") } + }; + + [Theory] + [MemberData(nameof(Fixture))] + public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializeHttpMethods( + string input, HttpMethod expected) + { + // arrange + var binary = MessagePackSerializer.Serialize(input, Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void HttpMethodMessagePackFormatter_Deserialize_ShouldHandleNull() + { + // arrange + var binary = MessagePackSerializer.Serialize(null, Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + // assert + result.Should().BeNull(); + } +} diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs new file mode 100644 index 0000000..b2bfabf --- /dev/null +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs @@ -0,0 +1,75 @@ +using System.Runtime.Serialization; +using MessagePack; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Tests.Internal; + +public enum Foo +{ + Foo = 0, + BAR = 1, + [EnumMember(Value = "baz_cux")] + BazCux = 2 +} + +public sealed class MessagePackStringEnumMemberFormatterTests +{ + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create( + new MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy.SnakeCase) + ), + BuiltinResolver.Instance + ) + ); + + public static IEnumerable + Fixtures => + new List + { + new object[] { "foo", Foo.Foo }, + new object[] { "bar", Foo.BAR }, + new object[] { "baz_cux", Foo.BazCux } + }; + + public static IEnumerable + WrongValueFixtures => + new List + { + new object[] { null }, + new object[] { "5" }, + new object[] { "BazCux" } + }; + + [Theory] + [MemberData(nameof(Fixtures))] + public void MessagePackStringEnumFormatter_Serialize_ShouldCorrectlySerialize( + string input, Foo expected) + { + // arrange + var binary = MessagePackSerializer.Serialize(input, Options); + + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(WrongValueFixtures))] + public void MessagePackStringEnumFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(string input) + { + // arrange + var binary = MessagePackSerializer.Serialize(input, Options); + + // act + var act = () => MessagePackSerializer.Deserialize(binary, Options); + + // assert + act.Should().Throw().WithMessage( + "Failed to deserialize*"); + } +} From e4a32d0d30bc23de1d56ac7105bd4253560bf580 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Fri, 31 May 2024 20:36:36 +0400 Subject: [PATCH 10/13] feat(repeater): address pr comments closes #170 --- src/SecTester.Repeater/Bus/IncomingRequest.cs | 56 +++++-------- .../Bus/OutgoingResponse.cs | 13 ++- .../MessagePackHttpHeadersFormatter.cs | 8 +- .../SecTester.Repeater.csproj | 1 - .../Bus/IncomingRequestTests.cs | 41 ++++++++-- .../Bus/SocketIoRepeaterBusTests.cs | 5 +- ...efaultMessagePackSerializerOptionsTests.cs | 82 ------------------- .../MessagePackHttpHeadersFormatterTests.cs | 41 +--------- 8 files changed, 75 insertions(+), 172 deletions(-) delete mode 100644 test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index 017a6e5..0d1928e 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -27,15 +27,20 @@ public record IncomingRequest(Uri Url) : IRequest Value = (Protocol)field.GetValue(null), StringValue = field.GetCustomAttribute()?.Value ?? field.Name }) - .ToDictionary(x => x.StringValue, x => x.Value); - + .ToDictionary(x => MessagePackNamingPolicy.SnakeCase.ConvertName(x.StringValue), x => x.Value); [Key(ProtocolKey)] public Protocol Protocol { get; set; } = Protocol.Http; + private IEnumerable>> _headers = Enumerable.Empty>>(); + [Key(HeadersKey)] - public IEnumerable>> Headers { get; set; } = - new List>>(); + public IEnumerable>> Headers + { + get => _headers; + // ADHOC: convert from a kind of assignable type to formatter resolvable type + set => _headers = value.AsEnumerable(); + } [Key(BodyKey)] public string? Body { get; set; } @@ -48,9 +53,11 @@ public record IncomingRequest(Uri Url) : IRequest public static IncomingRequest FromDictionary(Dictionary dictionary) { - var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && ProtocolEntries.TryGetValue(p.ToString(), out var e) - ? e - : Protocol.Http; + var protocol = !dictionary.ContainsKey(ProtocolKey) || (dictionary.TryGetValue(ProtocolKey, out var p1) && p1 is null) + ? Protocol.Http + : dictionary.TryGetValue(ProtocolKey, out var p2) && p2 is string && ProtocolEntries.TryGetValue(p2.ToString(), out var e) + ? e + : throw new InvalidDataException(FormatPropertyError(ProtocolKey)); var uri = dictionary.TryGetValue(UrlKey, out var u) && u is string ? new Uri(u.ToString()) @@ -64,7 +71,7 @@ public static IncomingRequest FromDictionary(Dictionary dictiona var headers = dictionary.TryGetValue(HeadersKey, out var h) && h is Dictionary value ? MapHeaders(value) - : new List>>(); + : Enumerable.Empty>>(); return new IncomingRequest(uri) { @@ -75,32 +82,13 @@ public static IncomingRequest FromDictionary(Dictionary dictiona }; } - private static IEnumerable>> MapHeaders(Dictionary headers) - { - var result = new List>>(headers?.Count ?? 0); - - foreach (var kvp in headers) + private static IEnumerable>> MapHeaders(Dictionary headers) => + headers.Select(kvp => kvp.Value switch { - var key = kvp.Key.ToString(); - - switch (kvp.Value) - { - case null: - result.Add(new KeyValuePair>(key, new List())); - continue; - case string: - result.Add(new KeyValuePair>(key, new List - { kvp.Value.ToString() })); - continue; - case object[] objects: - result.Add(new KeyValuePair>(key, - objects.OfType().Select(value => value.ToString()).ToList())); - continue; - } - } - - return result; - } + IEnumerable strings => new KeyValuePair>(kvp.Key.ToString(), strings.Select(x => x.ToString())), + null => new KeyValuePair>(kvp.Key.ToString(), Enumerable.Empty()), + _ => new KeyValuePair>(kvp.Key.ToString(), new[] { kvp.Value.ToString() }) + }); - private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type"; + private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type or value"; } diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index f21f6ee..9378a4a 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Linq; using MessagePack; -using SecTester.Repeater.Internal; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; @@ -23,8 +23,13 @@ public record OutgoingResponse : IResponse [Key("errorCode")] public string? ErrorCode { get; set; } - [Key("headers")] - public IEnumerable>> Headers { get; set; } = - new List>>(); + private IEnumerable>> _headers = Enumerable.Empty>>(); + [Key("headers")] + public IEnumerable>> Headers + { + get => _headers; + // ADHOC: convert from a kind of assignable type to formatter resolvable type + set => _headers = value.AsEnumerable(); + } } diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs index 1c9edea..cbd6db9 100644 --- a/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs +++ b/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs @@ -10,10 +10,10 @@ namespace SecTester.Repeater.Internal; // https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format internal class MessagePackHttpHeadersFormatter : IMessagePackFormatter< - List>>? + IEnumerable>>? > { - public void Serialize(ref MessagePackWriter writer, List>>? value, + public void Serialize(ref MessagePackWriter writer, IEnumerable>>? value, MessagePackSerializerOptions options) { if (value == null) @@ -59,7 +59,7 @@ private static void Serialize(ref MessagePackWriter writer, KeyValuePair>>? Deserialize(ref MessagePackReader reader, + public IEnumerable>>? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { if (reader.NextMessagePackType == MessagePackType.Nil) @@ -87,7 +87,7 @@ private static void Serialize(ref MessagePackWriter writer, KeyValuePair>> DeserializeMap(ref MessagePackReader reader, int length, + private static IEnumerable>> DeserializeMap(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) { var result = new List>>(length); diff --git a/src/SecTester.Repeater/SecTester.Repeater.csproj b/src/SecTester.Repeater/SecTester.Repeater.csproj index 8f6038b..79094ef 100644 --- a/src/SecTester.Repeater/SecTester.Repeater.csproj +++ b/src/SecTester.Repeater/SecTester.Repeater.csproj @@ -17,7 +17,6 @@ - diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index 69e889f..0b5a43c 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -1,4 +1,5 @@ using MessagePack; +using SecTester.Repeater.Internal; using SocketIO.Core; using SocketIO.Serializer.MessagePack; @@ -6,6 +7,8 @@ namespace SecTester.Repeater.Tests.Bus; public class IncomingRequestTests { + private static readonly MessagePackSerializerOptions Options = DefaultMessagePackSerializerOptions.Instance; + private static readonly IEnumerable ValidFixtures = new[] { new IncomingRequest(new Uri("http://foo.bar/1")) @@ -37,11 +40,11 @@ public class IncomingRequestTests public void IncomingRequest_FromDictionary_ShouldCreateInstance(IncomingRequest input) { // arrange - var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + var serializer = new SocketIOMessagePackSerializer(Options); var serialized = serializer.Serialize(EngineIO.V4, "request", 1, "/some", new object[] { input }).First(); - var deserializedPackMessage = MessagePackSerializer.Deserialize(serialized.Binary); + var deserializedPackMessage = MessagePackSerializer.Deserialize(serialized.Binary ,Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -59,9 +62,9 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot var packJson = "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http:\",\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; - var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + var serializer = new SocketIOMessagePackSerializer(Options); - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson)); + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson) ,Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -79,9 +82,9 @@ public void IncomingRequest_FromDictionary_ShouldAssignDefaultPropertyValues() var packJson = "{\"type\":2,\"data\":[\"request\",{\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; - var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + var serializer = new SocketIOMessagePackSerializer(Options); - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson)); + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -93,15 +96,15 @@ public void IncomingRequest_FromDictionary_ShouldAssignDefaultPropertyValues() } [Fact] - public void IncomingRequest_FromDictionary_ShouldParseEnumNamedValue() + public void IncomingRequest_FromDictionary_ShouldParseProtocolValue() { // arrange var packJson = "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http\",\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; - var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard); + var serializer = new SocketIOMessagePackSerializer(Options); - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson)); + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson) ,Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -111,4 +114,24 @@ public void IncomingRequest_FromDictionary_ShouldParseEnumNamedValue() // assert result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")){ Protocol = Protocol.Http}); } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsNotParsable() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"ws\",\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(Options); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson) ,Options); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var act = () => IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + act.Should().Throw(); + } } diff --git a/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs b/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs index 1dc7206..17351e8 100644 --- a/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs @@ -36,7 +36,10 @@ public async Task RequestReceived_ExecutesHandler() StatusCode = 204 }; _connection.Connect().Returns(Task.CompletedTask); - _socketIoMessage.GetValue().Returns(new IncomingRequest(Url)); + _socketIoMessage.GetValue>().Returns(new Dictionary() + { + {"url", Url.ToString()} + }); _connection.On("request", Arg.Invoke(_socketIoMessage)); _sut.RequestReceived += _ => Task.FromResult(result); diff --git a/test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs b/test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs deleted file mode 100644 index b584537..0000000 --- a/test/SecTester.Repeater.Tests/Internal/DefaultMessagePackSerializerOptionsTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using MessagePack; -using SecTester.Repeater.Internal; - -namespace SecTester.Repeater.Tests.Internal; - -public sealed class DefaultMessagePackSerializerOptionsTests -{ - [MessagePackObject] - public record TestDto - { - [Key("protocol")] - public Protocol Protocol { get; set; } = Protocol.Http; - - [Key("method")] - public HttpMethod Method { get; set; } = HttpMethod.Delete; - - private IEnumerable>>? _headers; - - [Key("headers")] - public IEnumerable>>? Headers { - get => this._headers; - set => this._headers = null != value ? new List>>(value) : value; - } - } - - private static readonly MessagePackSerializerOptions Options = DefaultMessagePackSerializerOptions.Instance; - - private static IEnumerable< - TestDto> - Fixtures => - new [] - { - new TestDto - { - Protocol = Protocol.Http, - Method = HttpMethod.Put, - Headers = new List>> - { - new("content-type", new List { "application/json" }), - new("cache-control", new List { "no-cache", "no-store" }) - } - } - }; - - public static IEnumerable SerializeDeserializeFixtures => Fixtures - .Select((x) => new object?[] - { - x, x - }); - - [Theory] - [MemberData(nameof(SerializeDeserializeFixtures))] - public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - TestDto input, - TestDto expected) - { - // arrange - var serialized = MessagePackSerializer.Serialize(input, Options); - - // act - var result = MessagePackSerializer.Deserialize(serialized, Options); - - // assert - result.Should().BeEquivalentTo(expected); - } - - [Fact] - public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() - { - // arrange - var binary = MessagePackSerializer.ConvertFromJson("{}", Options); - - // act - var result = MessagePackSerializer.Deserialize(binary, Options); - - // assert - result.Should().BeEquivalentTo(new TestDto - { - Headers = null - }); - } -} diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs index 12bc2c8..59f20d4 100644 --- a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs @@ -21,7 +21,7 @@ public sealed class MessagePackHttpHeadersFormatterTests }, new object[] { - new List>>() + Enumerable.Empty>>() }, new object[] @@ -30,7 +30,7 @@ public sealed class MessagePackHttpHeadersFormatterTests { new("content-type", new List { "application/json" }), new("cache-control", new List { "no-cache", "no-store" }) - } + }.AsEnumerable() } }; @@ -50,48 +50,15 @@ public sealed class MessagePackHttpHeadersFormatterTests [Theory] [MemberData(nameof(Fixtures))] public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - List>>? input) + IEnumerable>>? input) { // arrange var serialized = MessagePackSerializer.Serialize(input, Options); // act - var result = MessagePackSerializer.Deserialize>>>(serialized, Options); + var result = MessagePackSerializer.Deserialize>>>(serialized, Options); // assert result.Should().BeEquivalentTo(input); } -/* - [Fact] - public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMissingValue() - { - // arrange - var binary = MessagePackSerializer.ConvertFromJson("{}", Options); - - // act - var result = MessagePackSerializer.Deserialize(binary, Options); - - // assert - result.Should().BeEquivalentTo(new HttpHeadersDto - { - Headers = null - }); - } - - [Theory] - [MemberData(nameof(WrongValueFixtures))] - public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue( - string input) - { - // arrange - var binary = MessagePackSerializer.ConvertFromJson(input, Options); - - // act - var act = () => MessagePackSerializer.Deserialize(binary, Options); - - // assert - act.Should().Throw().WithMessage( - "Failed to deserialize*"); - } - */ } From 3fe3e1d2cf37250f822df85f119b869f0d26bf5b Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Sat, 1 Jun 2024 16:40:56 +0700 Subject: [PATCH 11/13] feat(repeater): fix formatting get rid unwanted usings closes #170 --- src/SecTester.Repeater/Bus/SocketIoMessage.cs | 2 -- .../Internal/DefaultMessagePackSerializerOptions.cs | 1 - .../Bus/IncomingRequestTests.cs | 10 +++++----- .../Internal/MessagePackHttpMethodFormatterTests.cs | 13 ++++++------- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/SecTester.Repeater/Bus/SocketIoMessage.cs b/src/SecTester.Repeater/Bus/SocketIoMessage.cs index 12c6e43..d336db1 100644 --- a/src/SecTester.Repeater/Bus/SocketIoMessage.cs +++ b/src/SecTester.Repeater/Bus/SocketIoMessage.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SecTester.Repeater.Internal; using SocketIOClient; namespace SecTester.Repeater.Bus; diff --git a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs index 16755a9..7e341b6 100644 --- a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs +++ b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs @@ -1,6 +1,5 @@ using MessagePack; using MessagePack.Resolvers; -using SecTester.Repeater.Internal; namespace SecTester.Repeater.Internal; diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index 0b5a43c..e193382 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -44,7 +44,7 @@ public void IncomingRequest_FromDictionary_ShouldCreateInstance(IncomingRequest var serialized = serializer.Serialize(EngineIO.V4, "request", 1, "/some", new object[] { input }).First(); - var deserializedPackMessage = MessagePackSerializer.Deserialize(serialized.Binary ,Options); + var deserializedPackMessage = MessagePackSerializer.Deserialize(serialized.Binary, Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -64,7 +64,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot var serializer = new SocketIOMessagePackSerializer(Options); - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson) ,Options); + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -104,7 +104,7 @@ public void IncomingRequest_FromDictionary_ShouldParseProtocolValue() var serializer = new SocketIOMessagePackSerializer(Options); - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson) ,Options); + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); @@ -112,7 +112,7 @@ public void IncomingRequest_FromDictionary_ShouldParseProtocolValue() var result = IncomingRequest.FromDictionary(deserializedDictionary); // assert - result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")){ Protocol = Protocol.Http}); + result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")) { Protocol = Protocol.Http }); } [Fact] @@ -124,7 +124,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsNotParsable( var serializer = new SocketIOMessagePackSerializer(Options); - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson) ,Options); + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs index 13bd7df..93a1420 100644 --- a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs @@ -1,5 +1,4 @@ using MessagePack; -using MessagePack.Formatters; using MessagePack.Resolvers; using SecTester.Repeater.Internal; @@ -7,12 +6,12 @@ namespace SecTester.Repeater.Tests.Internal; public sealed class MessagePackHttpMethodFormatterTests { - private static readonly MessagePackSerializerOptions Options = new( - CompositeResolver.Create( - CompositeResolver.Create(new MessagePackHttpMethodFormatter()), - BuiltinResolver.Instance - ) - ); + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create(new MessagePackHttpMethodFormatter()), + BuiltinResolver.Instance + ) + ); public static readonly IEnumerable Fixture = new List { From fb0478d4bfa13791f543d7900f02a7dbb496057b Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Tue, 4 Jun 2024 18:26:20 +0700 Subject: [PATCH 12/13] feat(repeater): address pr comments closes #170 --- src/SecTester.Repeater/Protocol.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/SecTester.Repeater/Protocol.cs b/src/SecTester.Repeater/Protocol.cs index f35c880..4221d78 100644 --- a/src/SecTester.Repeater/Protocol.cs +++ b/src/SecTester.Repeater/Protocol.cs @@ -1,9 +1,6 @@ -using System.Runtime.Serialization; - namespace SecTester.Repeater; public enum Protocol { - [EnumMember(Value = "http")] Http } From 425e6d22640e9f065543b3a49619c655e2a6b958 Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Sat, 8 Jun 2024 01:58:24 +0400 Subject: [PATCH 13/13] refactor(repeater): simplify `FromDictionary` method --- src/SecTester.Repeater/Bus/IncomingRequest.cs | 106 ++++++++---------- .../Bus/OutgoingResponse.cs | 10 +- .../DefaultMessagePackSerializerOptions.cs | 2 +- .../Internal/HttpMethods.cs | 30 +++++ .../MessagePackHttpMethodFormatter.cs | 27 +---- .../MessagePackStringEnumMemberFormatter.cs | 12 +- .../SecTester.Repeater.csproj | 2 + .../Bus/IncomingRequestTests.cs | 24 +--- .../MessagePackHttpHeadersFormatterTests.cs | 9 +- 9 files changed, 94 insertions(+), 128 deletions(-) create mode 100644 src/SecTester.Repeater/Internal/HttpMethods.cs diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index 0d1928e..ff859fd 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net.Http; -using System.Reflection; -using System.Runtime.Serialization; using MessagePack; using SecTester.Repeater.Internal; using SecTester.Repeater.Runners; @@ -20,75 +17,66 @@ public record IncomingRequest(Uri Url) : IRequest private const string BodyKey = "body"; private const string ProtocolKey = "protocol"; - private static readonly Dictionary ProtocolEntries = typeof(Protocol) - .GetFields(BindingFlags.Public | BindingFlags.Static) - .Select(field => new - { - Value = (Protocol)field.GetValue(null), - StringValue = field.GetCustomAttribute()?.Value ?? field.Name - }) - .ToDictionary(x => MessagePackNamingPolicy.SnakeCase.ConvertName(x.StringValue), x => x.Value); - - [Key(ProtocolKey)] - public Protocol Protocol { get; set; } = Protocol.Http; + [Key(ProtocolKey)] public Protocol Protocol { get; set; } = Protocol.Http; - private IEnumerable>> _headers = Enumerable.Empty>>(); - - [Key(HeadersKey)] - public IEnumerable>> Headers - { - get => _headers; - // ADHOC: convert from a kind of assignable type to formatter resolvable type - set => _headers = value.AsEnumerable(); - } + [Key(HeadersKey)] public IEnumerable>> Headers { get; set; } = new Dictionary>(); - [Key(BodyKey)] - public string? Body { get; set; } + [Key(BodyKey)] public string? Body { get; set; } - [Key(MethodKey)] - public HttpMethod Method { get; set; } = HttpMethod.Get; + [Key(MethodKey)] public HttpMethod Method { get; set; } = HttpMethod.Get; - [Key(UrlKey)] - public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); + [Key(UrlKey)] public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); public static IncomingRequest FromDictionary(Dictionary dictionary) { - var protocol = !dictionary.ContainsKey(ProtocolKey) || (dictionary.TryGetValue(ProtocolKey, out var p1) && p1 is null) - ? Protocol.Http - : dictionary.TryGetValue(ProtocolKey, out var p2) && p2 is string && ProtocolEntries.TryGetValue(p2.ToString(), out var e) - ? e - : throw new InvalidDataException(FormatPropertyError(ProtocolKey)); - - var uri = dictionary.TryGetValue(UrlKey, out var u) && u is string - ? new Uri(u.ToString()) - : throw new InvalidDataException(FormatPropertyError(UrlKey)); - - var method = dictionary.TryGetValue(MethodKey, out var m) && m is string - ? new HttpMethod(m.ToString()) - : HttpMethod.Get; + var protocol = GetProtocolFromDictionary(dictionary); + var headers = GetHeadersFromDictionary(dictionary); + var body = GetBodyFromDictionary(dictionary); + var method = GetMethodFromDictionary(dictionary); + var url = GetUrlFromDictionary(dictionary); - var body = dictionary.TryGetValue(BodyKey, out var b) && b is string ? b.ToString() : null; - - var headers = dictionary.TryGetValue(HeadersKey, out var h) && h is Dictionary value - ? MapHeaders(value) - : Enumerable.Empty>>(); - - return new IncomingRequest(uri) + return new IncomingRequest(url!) { Protocol = protocol, + Headers = headers, Body = body, - Method = method, - Headers = headers + Method = method }; } - private static IEnumerable>> MapHeaders(Dictionary headers) => - headers.Select(kvp => kvp.Value switch - { - IEnumerable strings => new KeyValuePair>(kvp.Key.ToString(), strings.Select(x => x.ToString())), - null => new KeyValuePair>(kvp.Key.ToString(), Enumerable.Empty()), - _ => new KeyValuePair>(kvp.Key.ToString(), new[] { kvp.Value.ToString() }) - }); + private static Protocol GetProtocolFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(ProtocolKey, out var protocolObj) && protocolObj is string protocolStr + ? (Protocol)Enum.Parse(typeof(Protocol), protocolStr, true) + : Protocol.Http; + + private static IEnumerable>> GetHeadersFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(HeadersKey, out var headersObj) && headersObj is Dictionary headersDict + ? ConvertToHeaders(headersDict) + : new Dictionary>(); + + private static string? GetBodyFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(BodyKey, out var bodyObj) ? bodyObj?.ToString() : null; + + private static HttpMethod GetMethodFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(MethodKey, out var methodObj) && methodObj is string methodStr + ? HttpMethods.Items.TryGetValue(methodStr, out var m) && m is not null + ? m + : HttpMethod.Get + : HttpMethod.Get; - private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type or value"; + private static Uri? GetUrlFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(UrlKey, out var urlObj) && urlObj is string urlStr + ? new Uri(urlStr) + : null; + + private static IEnumerable>> ConvertToHeaders(Dictionary headers) => + headers.ToDictionary( + kvp => kvp.Key.ToString()!, + kvp => kvp.Value switch + { + IEnumerable list => list.Select(v => v.ToString()!), + string str => new[] { str }, + _ => Enumerable.Empty() + } + ); } diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index 9378a4a..70a8658 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using MessagePack; using SecTester.Repeater.Runners; @@ -23,13 +22,6 @@ public record OutgoingResponse : IResponse [Key("errorCode")] public string? ErrorCode { get; set; } - private IEnumerable>> _headers = Enumerable.Empty>>(); - [Key("headers")] - public IEnumerable>> Headers - { - get => _headers; - // ADHOC: convert from a kind of assignable type to formatter resolvable type - set => _headers = value.AsEnumerable(); - } + public IEnumerable>> Headers { get; set; } = new Dictionary>(); } diff --git a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs index 7e341b6..707b476 100644 --- a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs +++ b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs @@ -3,7 +3,7 @@ namespace SecTester.Repeater.Internal; -internal class DefaultMessagePackSerializerOptions +internal static class DefaultMessagePackSerializerOptions { internal static readonly MessagePackSerializerOptions Instance = new( CompositeResolver.Create( diff --git a/src/SecTester.Repeater/Internal/HttpMethods.cs b/src/SecTester.Repeater/Internal/HttpMethods.cs new file mode 100644 index 0000000..1900399 --- /dev/null +++ b/src/SecTester.Repeater/Internal/HttpMethods.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; + +namespace SecTester.Repeater.Internal; + +public class HttpMethods +{ + public static IDictionary Items { get; } = typeof(HttpMethod) + .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(x => x.PropertyType.IsAssignableFrom(typeof(HttpMethod))) + .Select(x => x.GetValue(null)) + .Cast() + .Concat(new List + { + new("PATCH"), + new("COPY"), + new("LINK"), + new("UNLINK"), + new("PURGE"), + new("LOCK"), + new("UNLOCK"), + new("PROPFIND"), + new("VIEW") + }) + .Distinct() + .ToDictionary(x => x.Method, x => x, StringComparer.InvariantCultureIgnoreCase); +} diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs index d362a64..0fc6569 100644 --- a/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs +++ b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Reflection; using MessagePack; using MessagePack.Formatters; @@ -10,27 +6,6 @@ namespace SecTester.Repeater.Internal; internal class MessagePackHttpMethodFormatter : IMessagePackFormatter { - private static readonly IEnumerable BaseMethods = typeof(HttpMethod) - .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Where(x => x.PropertyType.IsAssignableFrom(typeof(HttpMethod))) - .Select(x => x.GetValue(null)) - .Cast(); - - private static readonly IEnumerable CustomMethods = new List - { - new("PATCH"), - new("COPY"), - new("LINK"), - new("UNLINK"), - new("PURGE"), - new("LOCK"), - new("UNLOCK"), - new("PROPFIND"), - new("VIEW") - }; - - private static readonly IDictionary Methods = BaseMethods.Concat(CustomMethods).Distinct() - .ToDictionary(x => x.Method, x => x, StringComparer.InvariantCultureIgnoreCase); public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePackSerializerOptions options) { if (null == value) @@ -63,7 +38,7 @@ public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePa { var token = reader.ReadString(); - if (token is null || !Methods.TryGetValue(token, out var method)) + if (token is null || !HttpMethods.Items.TryGetValue(token, out var method)) { throw new MessagePackSerializationException( $"Unexpected value {token} when parsing the {nameof(HttpMethod)}."); diff --git a/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs index 1db48fa..064e40c 100644 --- a/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs +++ b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs @@ -20,18 +20,18 @@ internal class MessagePackStringEnumMemberFormatter : IMessagePackFormatter x.Value, x => x.StringValue); - private readonly Dictionary CasedStringToEnum; - private readonly Dictionary CasedEnumToString; + private readonly Dictionary _casedStringToEnum; + private readonly Dictionary _casedEnumToString; public MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy namingPolicy) { - this.CasedEnumToString = EnumToString.ToDictionary(x => x.Key, x => namingPolicy.ConvertName(x.Value)); - this.CasedStringToEnum = EnumToString.ToDictionary(x => namingPolicy.ConvertName(x.Value), x => x.Key); + this._casedEnumToString = EnumToString.ToDictionary(x => x.Key, x => namingPolicy.ConvertName(x.Value)); + this._casedStringToEnum = EnumToString.ToDictionary(x => namingPolicy.ConvertName(x.Value), x => x.Key); } public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options) { - if (!CasedEnumToString.TryGetValue(value, out var stringValue)) + if (!_casedEnumToString.TryGetValue(value, out var stringValue)) { throw new MessagePackSerializationException($"No string representation found for {value}"); } @@ -43,7 +43,7 @@ public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions { var stringValue = reader.ReadString(); - if (null == stringValue || !CasedStringToEnum.TryGetValue(stringValue, out var enumValue)) + if (null == stringValue || !_casedStringToEnum.TryGetValue(stringValue, out var enumValue)) { throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); } diff --git a/src/SecTester.Repeater/SecTester.Repeater.csproj b/src/SecTester.Repeater/SecTester.Repeater.csproj index 79094ef..1bbf68f 100644 --- a/src/SecTester.Repeater/SecTester.Repeater.csproj +++ b/src/SecTester.Repeater/SecTester.Repeater.csproj @@ -17,7 +17,9 @@ + + diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index e193382..672bcc3 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -56,7 +56,7 @@ public void IncomingRequest_FromDictionary_ShouldCreateInstance(IncomingRequest } [Fact] - public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNotProvided() + public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsInvalid() { // arrange var packJson = @@ -72,7 +72,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot var act = () => IncomingRequest.FromDictionary(deserializedDictionary); // assert - act.Should().Throw(); + act.Should().Throw(); } [Fact] @@ -114,24 +114,4 @@ public void IncomingRequest_FromDictionary_ShouldParseProtocolValue() // assert result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")) { Protocol = Protocol.Http }); } - - [Fact] - public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsNotParsable() - { - // arrange - var packJson = - "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"ws\",\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; - - var serializer = new SocketIOMessagePackSerializer(Options); - - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); - - var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); - - // act - var act = () => IncomingRequest.FromDictionary(deserializedDictionary); - - // assert - act.Should().Throw(); - } } diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs index 59f20d4..68d29cf 100644 --- a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs @@ -13,24 +13,23 @@ public sealed class MessagePackHttpHeadersFormatterTests ) ); - public static readonly IEnumerable Fixtures = new List() + public static readonly IEnumerable Fixtures = new List() { - new object[] + new object?[] { null }, new object[] { Enumerable.Empty>>() - }, - new object[] + new object?[] { new List>> { new("content-type", new List { "application/json" }), new("cache-control", new List { "no-cache", "no-store" }) - }.AsEnumerable() + } } };