using AyCode.Core.Extensions; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using MessagePack.Resolvers; using AyCode.Core.Tests.Serialization; namespace AyCode.Services.Server.Tests.SignalRs; /// /// Base class for SignalR client-to-hub communication tests. /// Tests the full round-trip: Client -> Server -> Service -> Response -> Client /// Derived classes specify the serializer type (JSON or Binary). /// public abstract class SignalRClientToHubTestBase { protected abstract AcSerializerOptions SerializerOption { get; } protected TestLogger _logger = null!; protected TestableSignalRClient2 _client = null!; protected TestableSignalRHub2 _hub = null!; protected TestSignalRService2 _service = null!; [TestInitialize] public void Setup() { _logger = new TestLogger(); _hub = new TestableSignalRHub2(); _service = new TestSignalRService2(); _client = new TestableSignalRClient2(_hub, _logger); _hub.SetSerializerType(SerializerOption); _hub.RegisterService(_service, _client); } #region Primitive Parameter Tests [TestMethod] [DataRow(42)] [DataRow(0)] [DataRow(-100)] [DataRow(int.MaxValue)] public async Task Post_SingleInt_ReturnsStringRepresentation(int value) { var result = await _client.PostDataAsync(TestSignalRTags.SingleIntParam, value); Assert.AreEqual(value.ToString(), result); } [TestMethod] [DataRow(10, 20, 30)] [DataRow(0, 0, 0)] [DataRow(-5, 10, 5)] public async Task Post_TwoInts_ReturnsSum(int a, int b, int expectedSum) { var result = await _client.PostAsync(TestSignalRTags.TwoIntParams, [a, b]); Assert.AreEqual(expectedSum, result); } [TestMethod] [DataRow(true)] [DataRow(false)] public async Task Post_Bool_ReturnsSameValue(bool value) { var result = await _client.PostDataAsync(TestSignalRTags.BoolParam, value); Assert.AreEqual(value, result); } [TestMethod] [DataRow("Hello")] [DataRow("")] [DataRow("Special chars: <>\"'&")] public async Task Post_String_ReturnsEcho(string text) { var result = await _client.PostDataAsync(TestSignalRTags.StringParam, text); Assert.AreEqual($"Echo: {text}", result); } [TestMethod] public async Task Post_Guid_ReturnsSameValue() { var guid = Guid.NewGuid(); var result = await _client.PostDataAsync(TestSignalRTags.GuidParam, guid); Assert.AreEqual(guid, result); } [TestMethod] public async Task Post_GuidEmpty_ReturnsSameValue() { var result = await _client.PostDataAsync(TestSignalRTags.GuidParam, Guid.Empty); Assert.AreEqual(Guid.Empty, result); } [TestMethod] [DataRow(TestStatus.Pending, TestStatus.Pending)] [DataRow(TestStatus.Processing, TestStatus.Processing)] [DataRow(TestStatus.Completed, TestStatus.Completed)] public async Task Post_Enum_ReturnsSameValue(TestStatus input, TestStatus expected) { var result = await _client.PostDataAsync(TestSignalRTags.EnumParam, input); Assert.AreEqual(expected, result); } [TestMethod] public async Task Get_NoParams_ReturnsOK() { var result = await _client.GetAllAsync(TestSignalRTags.NoParams); Assert.AreEqual("OK", result); } [TestMethod] public async Task Post_MultipleTypes_ReturnsConcatenated() { var result = await _client.PostAsync(TestSignalRTags.MultipleTypesParams, [true, "test", 42]); Assert.AreEqual("True-test-42", result); } [TestMethod] [DataRow(123.45)] [DataRow(0.0)] [DataRow(-99.99)] public async Task Post_Decimal_ReturnsDoubled(double inputDouble) { var input = (decimal)inputDouble; var result = await _client.PostDataAsync(TestSignalRTags.DecimalParam, input); Assert.AreEqual(input * 2, result); } [TestMethod] public async Task Post_DateTime_ReturnsSameValue() { var dateTime = new DateTime(2024, 6, 15, 10, 30, 45, DateTimeKind.Utc); var result = await _client.PostDataAsync(TestSignalRTags.DateTimeParam, dateTime); Assert.AreEqual(dateTime, result); } [TestMethod] [DataRow(3.14159)] [DataRow(0.0)] [DataRow(-1.5)] public async Task Post_Double_ReturnsSameValue(double value) { var result = await _client.PostDataAsync(TestSignalRTags.DoubleParam, value); Assert.AreEqual(value, result, 0.0001); } [TestMethod] [DataRow(9223372036854775807L)] [DataRow(0L)] [DataRow(-1L)] public async Task Post_Long_ReturnsSameValue(long value) { var result = await _client.PostDataAsync(TestSignalRTags.LongParam, value); Assert.AreEqual(value, result); } #endregion #region Complex Object Tests [TestMethod] public async Task Post_TestOrderItem_ReturnsProcessedItem() { var item = new TestOrderItem { Id = 1, ProductName = "Widget", Quantity = 5, UnitPrice = 10.50m }; var result = await _client.PostDataAsync(TestSignalRTags.TestOrderItemParam, item); Assert.IsNotNull(result); Assert.AreEqual(1, result.Id); Assert.AreEqual("Processed: Widget", result.ProductName); Assert.AreEqual(item.Quantity * 2, result.Quantity); Assert.AreEqual(item.UnitPrice * 2, result.UnitPrice); } [TestMethod] public async Task Post_TestOrder_ReturnsSameOrder() { var order = TestDataFactory.CreateOrder(itemCount: 2); var result = await _client.PostDataAsync(TestSignalRTags.TestOrderParam, order); Assert.IsNotNull(result); Assert.AreEqual(order.Id, result.Id); Assert.AreEqual(order.OrderNumber, result.OrderNumber); Assert.AreEqual(2, result.Items.Count); } [TestMethod] public async Task Post_SharedTag_ReturnsSameTag() { var tag = new SharedTag { Id = 1, Name = "Important", Color = "#FF0000" }; var result = await _client.PostDataAsync(TestSignalRTags.SharedTagParam, tag); Assert.IsNotNull(result); Assert.AreEqual(1, result.Id); Assert.AreEqual("Important", result.Name); Assert.AreEqual("#FF0000", result.Color); } #endregion #region Collection Parameter Tests [TestMethod] public async Task Post_IntArray_ReturnsDoubledValues() { var input = new[] { 1, 2, 3, 4, 5 }; var result = await _client.PostDataAsync(TestSignalRTags.IntArrayParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(new[] { 2, 4, 6, 8, 10 }, result); } [TestMethod] public async Task Post_GuidArray_ReturnsSameValues() { var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; var result = await _client.PostDataAsync(TestSignalRTags.GuidArrayParam, guids); Assert.IsNotNull(result); CollectionAssert.AreEqual(guids, result); } [TestMethod] public async Task Post_StringList_ReturnsUppercased() { var input = new List { "apple", "banana", "cherry" }; var result = await _client.PostDataAsync, List>(TestSignalRTags.StringListParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(new List { "APPLE", "BANANA", "CHERRY" }, result); } [TestMethod] public async Task Post_TestOrderItemList_ReturnsSameItems() { var items = new List { new() { Id = 1, ProductName = "Item1" }, new() { Id = 2, ProductName = "Item2" } }; var result = await _client.PostDataAsync, List>(TestSignalRTags.TestOrderItemListParam, items); Assert.IsNotNull(result); Assert.AreEqual(2, result.Count); Assert.AreEqual("Item1", result[0].ProductName); Assert.AreEqual("Item2", result[1].ProductName); } [TestMethod] public async Task Post_IntList_ReturnsDoubledValues() { var input = new List { 10, 20, 30 }; var result = await _client.PostDataAsync, List>(TestSignalRTags.IntListParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(new List { 20, 40, 60 }, result); } [TestMethod] public async Task Post_BoolArray_ReturnsSameValues() { var input = new[] { true, false, true, false }; var result = await _client.PostDataAsync(TestSignalRTags.BoolArrayParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(input, result); } [TestMethod] public async Task Post_MixedWithArray_ReturnsFormattedString() { var numbers = new[] { 1, 2, 3 }; var result = await _client.PostAsync(TestSignalRTags.MixedWithArrayParam, [true, numbers, "end"]); Assert.AreEqual("True-[1,2,3]-end", result); } [TestMethod] public async Task Post_NestedList_ReturnsSameStructure() { var nested = new List> { new() { 1, 2, 3 }, new() { 4, 5 }, new() { 6 } }; var result = await _client.PostDataAsync>, List>>(TestSignalRTags.NestedListParam, nested); Assert.IsNotNull(result); Assert.AreEqual(3, result.Count); CollectionAssert.AreEqual(new List { 1, 2, 3 }, result[0]); CollectionAssert.AreEqual(new List { 4, 5 }, result[1]); CollectionAssert.AreEqual(new List { 6 }, result[2]); } #endregion #region Extended Array Tests [TestMethod] public async Task Post_LongArray_ReturnsSameValues() { var input = new[] { 1L, 2L, long.MaxValue }; var result = await _client.PostDataAsync(TestSignalRTags.LongArrayParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(input, result); } [TestMethod] public async Task GetAll_WithEmptyArrayContextParam_ReturnsResult() { var result = await _client.GetAllAsync(TestSignalRTags.IntArrayParam, [Array.Empty()]); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(0, result.Length, "Empty array should return empty array"); } [TestMethod] public async Task GetAll_WithArrayContextParam_ReturnsDoubledValues() { var input = new[] { 1, 2, 3 }; var result = await _client.GetAllAsync(TestSignalRTags.IntArrayParam, [input]); Assert.IsNotNull(result, "Result should not be null"); CollectionAssert.AreEqual(new[] { 2, 4, 6 }, result); } [TestMethod] public async Task Post_DecimalArray_ReturnsSameValues() { var input = new[] { 1.1m, 2.2m, 3.3m }; var result = await _client.PostDataAsync(TestSignalRTags.DecimalArrayParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(input, result); } [TestMethod] public async Task Post_DateTimeArray_ReturnsSameValues() { var input = new[] { new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), new DateTime(2024, 6, 15, 12, 30, 0, DateTimeKind.Utc) }; var result = await _client.PostDataAsync(TestSignalRTags.DateTimeArrayParam, input); Assert.IsNotNull(result); Assert.AreEqual(2, result.Length); Assert.AreEqual(input[0], result[0]); Assert.AreEqual(input[1], result[1]); } [TestMethod] public async Task Post_EnumArray_ReturnsSameValues() { var input = new[] { TestStatus.Pending, TestStatus.Completed, TestStatus.Processing }; var result = await _client.PostDataAsync(TestSignalRTags.EnumArrayParam, input); Assert.IsNotNull(result); CollectionAssert.AreEqual(input, result); } [TestMethod] public async Task Post_DoubleArray_ReturnsSameValues() { var input = new[] { 1.1, 2.2, 3.3 }; var result = await _client.PostDataAsync(TestSignalRTags.DoubleArrayParam, input); Assert.IsNotNull(result); Assert.AreEqual(3, result.Length); Assert.AreEqual(1.1, result[0], 0.001); Assert.AreEqual(2.2, result[1], 0.001); Assert.AreEqual(3.3, result[2], 0.001); } [TestMethod] public async Task Post_SharedTagArray_ReturnsSameValues() { var input = new[] { new SharedTag { Id = 1, Name = "Tag1", Color = "#FF0000" }, new SharedTag { Id = 2, Name = "Tag2", Color = "#00FF00" } }; var result = await _client.PostDataAsync(TestSignalRTags.SharedTagArrayParam, input); Assert.IsNotNull(result); Assert.AreEqual(2, result.Length); Assert.AreEqual("Tag1", result[0].Name); Assert.AreEqual("Tag2", result[1].Name); } [TestMethod] public async Task Post_Dictionary_ReturnsSameValues() { var input = new Dictionary { { "one", 1 }, { "two", 2 }, { "three", 3 } }; var result = await _client.PostDataAsync, Dictionary>(TestSignalRTags.DictionaryParam, input); Assert.IsNotNull(result); Assert.AreEqual(3, result.Count); Assert.AreEqual(1, result["one"]); Assert.AreEqual(2, result["two"]); Assert.AreEqual(3, result["three"]); } #endregion #region Mixed Parameter Tests [TestMethod] public async Task Post_IntAndDto_ReturnsFormattedString() { var item = new TestOrderItem { Id = 1, ProductName = "Widget" }; var result = await _client.PostAsync(TestSignalRTags.IntAndDtoParam, [42, item]); Assert.AreEqual("42-Widget", result); } [TestMethod] public async Task Post_DtoAndList_ReturnsFormattedString() { var item = new TestOrderItem { Id = 1, ProductName = "Product" }; var numbers = new List { 1, 2, 3 }; var result = await _client.PostAsync(TestSignalRTags.DtoAndListParam, [item, numbers]); Assert.AreEqual("Product-[1,2,3]", result); } [TestMethod] public async Task Post_ThreeComplexParams_ReturnsFormattedString() { var item = new TestOrderItem { Id = 1, ProductName = "Item" }; var tags = new List { "tag1", "tag2", "tag3" }; var sharedTag = new SharedTag { Id = 1, Name = "Shared" }; var result = await _client.PostAsync(TestSignalRTags.ThreeComplexParams, [item, tags, sharedTag]); Assert.AreEqual("Item-3-Shared", result); } [TestMethod] public async Task Post_FiveParams_ReturnsFormattedString() { var guid = Guid.Parse("12345678-1234-1234-1234-123456789abc"); var result = await _client.PostAsync(TestSignalRTags.FiveParams, [42, "hello", true, guid, 99.99m]); Assert.IsNotNull(result); Assert.AreEqual($"42-hello-True-{guid}-99.99", result); } [TestMethod] public async Task GetByIdAsync_FiveParams_ReturnsFormattedString() { var guid = Guid.NewGuid(); var result = await _client.GetByIdAsync(TestSignalRTags.FiveParams, [1, "text", true, guid, 99.99m]); Assert.IsNotNull(result); Assert.AreEqual($"1-text-True-{guid}-99.99", result); } #endregion #region Error Handling Tests [TestMethod] public async Task Post_ThrowsException_ReturnsError() { var result = await _client.GetAllAsync(TestSignalRTags.ThrowsException); Assert.IsNull(result); Assert.IsTrue(_logger.HasErrorLogs); } #endregion #region Async Task Method Tests [TestMethod] public async Task Async_TestOrderItem_ReturnsProcessedItem() { var item = new TestOrderItem { Id = 1, ProductName = "Widget", Quantity = 5, UnitPrice = 10.50m }; var result = await _client.PostDataAsync(TestSignalRTags.AsyncTestOrderItemParam, item); Assert.IsNotNull(result); Assert.AreEqual(1, result.Id); Assert.AreEqual("Async: Widget", result.ProductName); Assert.AreEqual(item.Quantity * 3, result.Quantity); Assert.AreEqual(item.UnitPrice * 3, result.UnitPrice); } [TestMethod] public async Task Async_String_ReturnsProcessedString() { var result = await _client.PostDataAsync(TestSignalRTags.AsyncStringParam, "TestInput"); Assert.IsNotNull(result); Assert.AreEqual("Async: TestInput", result); } [TestMethod] public async Task Async_NoParams_ReturnsAsyncOK() { var result = await _client.GetAllAsync(TestSignalRTags.AsyncNoParams); Assert.IsNotNull(result); Assert.AreEqual("AsyncOK", result); } [TestMethod] public async Task Async_Int_ReturnsDoubledValue() { var result = await _client.PostDataAsync(TestSignalRTags.AsyncIntParam, 42); Assert.AreEqual(84, result); } #endregion #region Round-Trip Integrity Tests [TestMethod] public async Task RoundTrip_ComplexObject_PreservesAllProperties() { var item = new TestOrderItem { Id = 999, ProductName = "RoundTrip Test Item", Quantity = 50, UnitPrice = 123.45m, Status = TestStatus.Processing }; var result = await _client.PostDataAsync(TestSignalRTags.TestOrderItemParam, item); Assert.IsNotNull(result); Assert.AreEqual(999, result.Id); Assert.AreEqual("Processed: RoundTrip Test Item", result.ProductName); Assert.AreEqual(100, result.Quantity); Assert.AreEqual(246.90m, result.UnitPrice); } [TestMethod] public async Task RoundTrip_NestedOrder_PreservesHierarchy() { var order = TestDataFactory.CreateOrder(itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 1); var result = await _client.PostDataAsync(TestSignalRTags.TestOrderParam, order); Assert.IsNotNull(result); Assert.AreEqual(order.Id, result.Id); Assert.AreEqual(order.OrderNumber, result.OrderNumber); Assert.AreEqual(3, result.Items.Count); for (int i = 0; i < 3; i++) { Assert.AreEqual(order.Items[i].Id, result.Items[i].Id); Assert.AreEqual(2, result.Items[i].Pallets.Count); } } [TestMethod] public async Task RoundTrip_SpecialCharacters_PreservedCorrectly() { var testString = "Special: \"quotes\" 'apostrophes' & ampersand \\ backslash \n newline"; var result = await _client.PostDataAsync(TestSignalRTags.StringParam, testString); Assert.IsNotNull(result); Assert.AreEqual($"Echo: {testString}", result); } [TestMethod] public async Task RoundTrip_UnicodeCharacters_PreservedCorrectly() { var testString = "Unicode: 中文 日本語 한국어 🎉 émoji"; var result = await _client.PostDataAsync(TestSignalRTags.StringParam, testString); Assert.IsNotNull(result); Assert.AreEqual($"Echo: {testString}", result); } [TestMethod] public async Task RoundTrip_EmptyString_PreservedCorrectly() { var result = await _client.PostDataAsync(TestSignalRTags.StringParam, ""); Assert.IsNotNull(result); Assert.AreEqual("Echo: ", result); } [TestMethod] public async Task RoundTrip_LargeDecimal_PreservedCorrectly() { var largeDecimal = 999999999999.999999m; var result = await _client.PostDataAsync(TestSignalRTags.DecimalParam, largeDecimal); Assert.AreEqual(largeDecimal * 2, result); } [TestMethod] public async Task RoundTrip_ExtremeInt_PreservedCorrectly() { var result = await _client.PostDataAsync(TestSignalRTags.SingleIntParam, int.MaxValue); Assert.AreEqual(int.MaxValue.ToString(), result); } #endregion #region Response Data Integrity Tests [TestMethod] public async Task ResponseData_NotDoubleEscaped() { var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 10, UnitPrice = 20m }; var result = await _client.PostDataAsync(TestSignalRTags.TestOrderItemParam, item); Assert.IsNotNull(result); Assert.AreEqual(1, result.Id); Assert.AreEqual("Processed: Test", result.ProductName); Assert.AreEqual(20, result.Quantity); Assert.AreEqual(40m, result.UnitPrice); } [TestMethod] public async Task CollectionResponse_DeserializesCorrectly() { var items = new List { new() { Id = 1, ProductName = "Item1", Quantity = 10, UnitPrice = 1.1m }, new() { Id = 2, ProductName = "Item2", Quantity = 20, UnitPrice = 2.2m }, new() { Id = 3, ProductName = "Item3", Quantity = 30, UnitPrice = 3.3m } }; var result = await _client.PostDataAsync, List>( TestSignalRTags.TestOrderItemListParam, items); Assert.IsNotNull(result); Assert.AreEqual(3, result.Count); for (int i = 0; i < 3; i++) { Assert.AreEqual(items[i].Id, result[i].Id); Assert.AreEqual(items[i].ProductName, result[i].ProductName); } } [TestMethod] public async Task Async_Method_ReturnsActualResult_NotTaskWrapper() { var item = new TestOrderItem { Id = 42, ProductName = "TestProduct", Quantity = 5, UnitPrice = 10m }; var result = await _client.PostDataAsync(TestSignalRTags.AsyncTestOrderItemParam, item); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(42, result.Id, "Id should match the original"); Assert.AreEqual("Async: TestProduct", result.ProductName, "ProductName should be 'Async: TestProduct', not null."); Assert.AreEqual(15, result.Quantity, "Quantity should be tripled (5*3=15)."); Assert.AreEqual(30m, result.UnitPrice, "UnitPrice should be tripled (10*3=30)."); } #endregion #region Task.FromResult Integration Tests [TestMethod] public async Task TaskFromResult_String_ReturnsActualResult_NotTaskWrapper() { var result = await _client.PostDataAsync(TestSignalRTags.TaskFromResultStringParam, "TestInput"); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual("FromResult: TestInput", result, "Should return the actual string, not Task wrapper JSON"); } [TestMethod] public async Task TaskFromResult_ComplexObject_ReturnsActualResult_NotTaskWrapper() { var item = new TestOrderItem { Id = 42, ProductName = "TestProduct", Quantity = 5, UnitPrice = 10m }; var result = await _client.PostDataAsync(TestSignalRTags.TaskFromResultTestOrderItemParam, item); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(42, result.Id, "Id should match the original"); Assert.AreEqual("FromResult: TestProduct", result.ProductName, "ProductName should be 'FromResult: TestProduct'."); Assert.AreEqual(10, result.Quantity, "Quantity should be doubled (5*2=10)."); Assert.AreEqual(20m, result.UnitPrice, "UnitPrice should be doubled (10*2=20)."); } [TestMethod] public async Task TaskFromResult_Int_ReturnsActualResult_NotTaskWrapper() { var result = await _client.PostDataAsync(TestSignalRTags.TaskFromResultIntParam, 42); Assert.AreEqual(84, result, "Should return doubled value (42*2=84)."); } [TestMethod] public async Task TaskFromResult_NoResult_CompletesSuccessfully() { var result = await _client.GetAllAsync(TestSignalRTags.TaskFromResultNoParams); } #endregion #region InvokeMethod Unit Tests [TestMethod] public void InvokeMethod_SyncMethod_ReturnsValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleSingleInt")!; var result = methodInfo.InvokeMethod(service, 42); Assert.AreEqual("42", result); } [TestMethod] public void InvokeMethod_AsyncTaskTMethod_ReturnsUnwrappedValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleAsyncString")!; var result = methodInfo.InvokeMethod(service, "Test"); Assert.IsNotNull(result, "InvokeMethod should unwrap Task and return the result"); Assert.AreEqual("Async: Test", result); } [TestMethod] public void InvokeMethod_AsyncTaskTMethod_WithComplexObject_ReturnsUnwrappedValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleAsyncTestOrderItem")!; var input = new TestOrderItem { Id = 1, ProductName = "Widget", Quantity = 5, UnitPrice = 10m }; var result = methodInfo.InvokeMethod(service, input); Assert.IsNotNull(result, "InvokeMethod should unwrap Task and return the result"); Assert.IsInstanceOfType(result, typeof(TestOrderItem)); var item = (TestOrderItem)result; Assert.AreEqual("Async: Widget", item.ProductName); Assert.AreEqual(15, item.Quantity); } [TestMethod] public void InvokeMethod_AsyncTaskTMethod_WithInt_ReturnsUnwrappedValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleAsyncInt")!; var result = methodInfo.InvokeMethod(service, 42); Assert.IsNotNull(result, "InvokeMethod should unwrap Task and return the result"); Assert.AreEqual(84, result); } [TestMethod] public void InvokeMethod_TaskFromResult_ReturnsUnwrappedValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleTaskFromResultString")!; var result = methodInfo.InvokeMethod(service, "Test"); Assert.IsNotNull(result, "InvokeMethod should unwrap Task.FromResult and return the result"); Assert.AreEqual("FromResult: Test", result); } [TestMethod] public void InvokeMethod_TaskFromResult_WithComplexObject_ReturnsUnwrappedValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleTaskFromResultTestOrderItem")!; var input = new TestOrderItem { Id = 1, ProductName = "Widget", Quantity = 5, UnitPrice = 10m }; var result = methodInfo.InvokeMethod(service, input); Assert.IsNotNull(result, "InvokeMethod should unwrap Task.FromResult and return the result"); Assert.IsInstanceOfType(result, typeof(TestOrderItem)); var item = (TestOrderItem)result; Assert.AreEqual("FromResult: Widget", item.ProductName); Assert.AreEqual(10, item.Quantity); } [TestMethod] public void InvokeMethod_TaskFromResult_WithInt_ReturnsUnwrappedValue() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleTaskFromResultInt")!; var result = methodInfo.InvokeMethod(service, 42); Assert.IsNotNull(result, "InvokeMethod should unwrap Task.FromResult and return the result"); Assert.AreEqual(84, result); } [TestMethod] public void InvokeMethod_NonGenericTask_CompletesWithoutError() { var service = new TestSignalRService2(); var methodInfo = typeof(TestSignalRService2).GetMethod("HandleTaskFromResultNoParams")!; var result = methodInfo.InvokeMethod(service); } #endregion #region GenericAttributes Tests [TestMethod] public async Task GenericAttributes_WithDateTimeString_PreservesExactValue() { var dto = new TestDtoWithGenericAttributes { Id = 123, Name = "Test Order", GenericAttributes = [ new TestGenericAttribute { Id = 1, Key = "DateOfReceipt", Value = "10/24/2025 00:27:00" }, new TestGenericAttribute { Id = 2, Key = "Priority", Value = "1" }, new TestGenericAttribute { Id = 3, Key = "SomeFlag", Value = "true" } ] }; var result = await _client.PostDataAsync( TestSignalRTags.GenericAttributesParam, dto); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(123, result.Id); Assert.AreEqual("Test Order", result.Name); Assert.AreEqual(3, result.GenericAttributes.Count); var dateAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "DateOfReceipt"); Assert.IsNotNull(dateAttr, "DateOfReceipt attribute should exist"); Assert.AreEqual("10/24/2025 00:27:00", dateAttr.Value); var priorityAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "Priority"); Assert.IsNotNull(priorityAttr); Assert.AreEqual("1", priorityAttr.Value); var flagAttr = result.GenericAttributes.FirstOrDefault(x => x.Key == "SomeFlag"); Assert.IsNotNull(flagAttr); Assert.AreEqual("true", flagAttr.Value); } [TestMethod] public async Task GenericAttributes_EmptyList_PreservesEmptyList() { var dto = new TestDtoWithGenericAttributes { Id = 789, Name = "Empty Attributes", GenericAttributes = [] }; var result = await _client.PostDataAsync( TestSignalRTags.GenericAttributesParam, dto); Assert.IsNotNull(result); Assert.AreEqual(789, result.Id); Assert.AreEqual("Empty Attributes", result.Name); Assert.IsNotNull(result.GenericAttributes); Assert.AreEqual(0, result.GenericAttributes.Count); } [TestMethod] public async Task GenericAttributes_ManyItems_AllPreserved() { var dto = new TestDtoWithGenericAttributes { Id = 999, Name = "Many Attributes", GenericAttributes = Enumerable.Range(0, 50).Select(i => new TestGenericAttribute { Id = i, Key = $"Attribute_{i}", Value = i % 5 == 0 ? $"{i}/24/2025 00:00:00" : i.ToString() }).ToList() }; var result = await _client.PostDataAsync( TestSignalRTags.GenericAttributesParam, dto); Assert.IsNotNull(result); Assert.AreEqual(50, result.GenericAttributes.Count); for (int i = 0; i < 50; i++) { var expectedValue = i % 5 == 0 ? $"{i}/24/2025 00:00:00" : i.ToString(); Assert.AreEqual($"Attribute_{i}", result.GenericAttributes[i].Key); Assert.AreEqual(expectedValue, result.GenericAttributes[i].Value); } } #endregion #region StockTaking Production Bug Reproduction /// /// CRITICAL PRODUCTION BUG TEST: Reproduces the exact GetStockTakings(false) scenario. /// Bug: "Type mismatch for property 'Created' (index 3/5) on 'StockTaking'" /// /// Root cause hypothesis: When IsClosed=false, the serializer skips it (default value optimization), /// but the first item has IsClosed=false while others have IsClosed=true. /// This may cause property index mismatch between server and client. /// Uses the REAL StockTaking model from StockTakingTestModels.cs /// [TestMethod] public async Task GetStockTakings_WithNullItems_RoundTrip() { // Simulate production: loadRelations = false var result = await _client.PostDataAsync>( TestSignalRTags.GetStockTakings, false); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(5, result.Count, "Should have 5 StockTakings from production DB"); // Verify first item (IsClosed = false - this is where the bug occurs!) var first = result[0]; Assert.AreEqual(7, first.Id, "First Id should be 7"); Assert.AreEqual(6, first.Creator, $"First Creator should be 6, got {first.Creator}. " + "If this is a very large number, DateTime bytes were interpreted as int!"); Assert.IsFalse(first.IsClosed, "First IsClosed should be false"); Assert.IsNull(first.StockTakingItems, "StockTakingItems should be null when loadRelations=false"); // Verify second item (IsClosed = true) var second = result[1]; Assert.AreEqual(6, second.Id, "Second Id should be 6"); Assert.AreEqual(6, second.Creator, "Second Creator should be 6"); Assert.IsTrue(second.IsClosed, "Second IsClosed should be true"); // Verify all items have correct Creator = 6 foreach (var item in result) { Assert.AreEqual(6, item.Creator, $"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}"); } } /// /// Test with loadRelations = true (StockTakingItems is empty list, not null). /// [TestMethod] public async Task GetStockTakings_WithEmptyItems_RoundTrip() { var result = await _client.PostDataAsync>( TestSignalRTags.GetStockTakings, true); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(5, result.Count, "Should have 5 StockTakings"); foreach (var item in result) { Assert.AreEqual(6, item.Creator, $"StockTaking Id={item.Id}: Creator should be 6, got {item.Creator}"); Assert.IsNotNull(item.StockTakingItems, $"StockTaking Id={item.Id}: StockTakingItems should not be null when loadRelations=true"); } } #endregion } /// /// Runs all SignalR tests with JSON serialization. /// [TestClass] public class SignalRClientToHubTest_Json : SignalRClientToHubTestBase { protected override AcSerializerOptions SerializerOption { get; } = new AcJsonSerializerOptions(); } /// /// Runs all SignalR tests with Binary serialization. /// [TestClass] public class SignalRClientToHubTest_Binary_WithRef : SignalRClientToHubTestBase { protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions(); } /// /// Runs all SignalR tests with Binary serialization. /// [TestClass] public class SignalRClientToHubTest_Binary_NoRef : SignalRClientToHubTestBase { protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions { UseReferenceHandling = false }; }