using AyCode.Core.Extensions; using AyCode.Core.Tests.TestModels; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using MessagePack.Resolvers; namespace AyCode.Services.Server.Tests.SignalRs; /// /// Integration tests for SignalR client-to-hub communication. /// Tests the full round-trip: Client -> Server -> Service -> Response -> Client /// [TestClass] public class SignalRClientToHubTest { private TestLogger _logger = null!; private TestableSignalRClient2 _client = null!; private TestableSignalRHub2 _hub = null!; private TestSignalRService2 _service = null!; [TestInitialize] public void Setup() { _logger = new TestLogger(); _hub = new TestableSignalRHub2(); _service = new TestSignalRService2(); _client = new TestableSignalRClient2(_hub, _logger); _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); // Doubled 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 (using PostDataAsync for complex types) [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); } /// /// REGRESSION TEST: Tests GetAllAsync with empty array as context parameter. /// Bug: When passing an empty array via GetAllAsync with contextParams, /// the server receives an object instead of an array, causing deserialization failure. /// Error: "JSON is an object but target type 'Int32[]' is a collection type" /// [TestMethod] public async Task GetAll_WithEmptyArrayContextParam_ReturnsResult() { var result = await _client.GetAllAsync(TestSignalRTags.IntArrayParam, [Array.Empty()]); Assert.IsNotNull(result, "Result should not be null"); // Empty array doubled is still empty array Assert.AreEqual(0, result.Length, "Empty array should return empty array"); } /// /// Tests GetAllAsync with non-empty array as context parameter. /// [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 - Critical for detecting non-awaited Tasks [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 MessagePack Round-Trip Integrity Tests [TestMethod] public async Task MessagePack_ComplexObject_PreservesAllProperties() { // Test that complex objects survive the full MessagePack round-trip 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); // Doubled by service Assert.AreEqual(246.90m, result.UnitPrice); // Doubled by service } [TestMethod] public async Task MessagePack_NestedOrder_PreservesHierarchy() { // Test deeply nested object structure survives round-trip 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); // Verify nested items preserved 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 MessagePack_SpecialCharacters_PreservedCorrectly() { // Test that special characters survive JSON serialization in MessagePack payload 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 MessagePack_UnicodeCharacters_PreservedCorrectly() { // Test that Unicode characters survive the round-trip var testString = "Unicode: 中文 日本語 한국어 🎉 émoji"; var result = await _client.PostDataAsync(TestSignalRTags.StringParam, testString); Assert.IsNotNull(result); Assert.AreEqual($"Echo: {testString}", result); } [TestMethod] public async Task MessagePack_EmptyString_PreservedCorrectly() { var result = await _client.PostDataAsync(TestSignalRTags.StringParam, ""); Assert.IsNotNull(result); Assert.AreEqual("Echo: ", result); } [TestMethod] public async Task MessagePack_LargeDecimal_PreservedCorrectly() { var largeDecimal = 999999999999.999999m; var result = await _client.PostDataAsync(TestSignalRTags.DecimalParam, largeDecimal); Assert.AreEqual(largeDecimal * 2, result); } [TestMethod] public async Task MessagePack_ExtremeInt_PreservedCorrectly() { var result = await _client.PostDataAsync(TestSignalRTags.SingleIntParam, int.MaxValue); Assert.AreEqual(int.MaxValue.ToString(), result); } #endregion #region JSON Serialization Integrity Tests [TestMethod] public async Task Json_ResponseData_NotDoubleEscaped() { // This test ensures the JSON in ResponseData is not double-escaped // which was the original bug we fixed var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 10, UnitPrice = 20m }; var result = await _client.PostDataAsync(TestSignalRTags.TestOrderItemParam, item); // If double serialization occurred, result would be null or have wrong values Assert.IsNotNull(result); Assert.AreEqual(1, result.Id); Assert.AreEqual("Processed: Test", result.ProductName); // Verify numeric values survived correctly (would fail with double-escaped JSON) Assert.AreEqual(20, result.Quantity); Assert.AreEqual(40m, result.UnitPrice); } [TestMethod] public async Task Json_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); } } /// /// CRITICAL TEST: Ensures async Task<T> methods are properly awaited before serialization. /// /// Bug scenario: If an async method returns Task<TestOrderItem> and the framework /// serializes the Task object instead of awaiting it, the JSON looks like: /// {"Result":{"Id":1,"ProductName":"..."},"Status":5,"IsCompleted":true,"IsCompletedSuccessfully":true} /// /// The actual data is wrapped in "Result" property, and Task metadata pollutes the response. /// This test ensures we get the actual object, not the Task wrapper. /// [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); // If Task was serialized instead of awaited, result would have wrong values: // - Id would still be correct (Task.Id coincidentally matches) // - ProductName would be null/empty (not at root level) // - Quantity would be 0 (not at root level) 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. " + "If null, the async method result was not properly awaited and Task was serialized."); Assert.AreEqual(15, result.Quantity, "Quantity should be tripled (5*3=15), not 0. " + "If 0, the async method result was not properly awaited."); Assert.AreEqual(30m, result.UnitPrice, "UnitPrice should be tripled (10*3=30), not 0. " + "If 0, the async method result was not properly awaited."); } #endregion #region Task.FromResult Integration Tests - CRITICAL for production bug coverage /// /// CRITICAL TEST: Tests full SignalR round-trip with Task.FromResult<string>. /// This was the root cause of the production bug where methods returning Task /// without async keyword caused Task wrapper to be serialized. /// [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"); } /// /// CRITICAL TEST: Tests full SignalR round-trip with Task.FromResult<TestOrderItem>. /// This simulates the exact production bug scenario where complex objects returned /// via Task.FromResult were serialized as Task wrapper instead of the actual object. /// /// Bug JSON output was: /// {"Result":{"Id":1,"ProductName":"..."},"Status":5,"IsCompleted":true,"IsCompletedSuccessfully":true} /// [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); // If Task wrapper was serialized, these assertions would fail: // - ProductName would be null (nested inside "Result" property) // - Quantity would be 0 // - UnitPrice would be 0 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'. " + "If null, Task.FromResult result was not properly unwrapped."); Assert.AreEqual(10, result.Quantity, "Quantity should be doubled (5*2=10). " + "If 0, Task.FromResult result was not properly unwrapped."); Assert.AreEqual(20m, result.UnitPrice, "UnitPrice should be doubled (10*2=20). " + "If 0, Task.FromResult result was not properly unwrapped."); } /// /// Tests Task.FromResult<int> - primitive value type via Task.FromResult. /// [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). " + "If 0 or wrong value, Task.FromResult was not properly unwrapped."); } /// /// Tests Task.CompletedTask (non-generic Task) via SignalR. /// [TestMethod] public async Task TaskFromResult_NoResult_CompletesSuccessfully() { // This should not throw and should complete successfully var result = await _client.GetAllAsync(TestSignalRTags.TaskFromResultNoParams); // Non-generic Task returns null as there's no result value // The important thing is the call completes without error } #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); } /// /// CRITICAL: Tests Task.FromResult() - methods returning Task without async keyword. /// This was the root cause of the production bug where Task wrapper was serialized. /// [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); // Doubled } [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")!; // Should complete without throwing var result = methodInfo.InvokeMethod(service); // Task.CompletedTask may return internal VoidTaskResult, the important thing is no exception } #endregion }