AyCode.Core/AyCode.Services.Server.Tests/SignalRs/TestSignalRService2.cs

556 lines
17 KiB
C#

using System.Globalization;
using AyCode.Core.Tests.TestModels;
using AyCode.Services.SignalRs;
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
using AyCode.Core.Tests.Serialization;
namespace AyCode.Services.Server.Tests.SignalRs;
/// <summary>
/// Test service with SignalR-attributed methods for testing ProcessOnReceiveMessage.
/// Uses shared DTOs from AyCode.Core.Tests.TestModels.
/// </summary>
public class TestSignalRService2
{
#region Primitive Parameter Handlers
[SignalR(TestSignalRTags.SingleIntParam)]
public string HandleSingleInt(int value)
{
return $"{value}";
}
[SignalR(TestSignalRTags.TwoIntParams)]
public int HandleTwoInts(int a, int b)
{
return a + b;
}
[SignalR(TestSignalRTags.BoolParam)]
public bool HandleBool(bool loadRelations)
{
return loadRelations;
}
[SignalR(TestSignalRTags.StringParam)]
public string HandleString(string text)
{
return $"Echo: {text}";
}
[SignalR(TestSignalRTags.GuidParam)]
public Guid HandleGuid(Guid id)
{
return id;
}
[SignalR(TestSignalRTags.EnumParam)]
public TestStatus HandleEnum(TestStatus status)
{
return status;
}
[SignalR(TestSignalRTags.NoParams)]
public string HandleNoParams()
{
return "OK";
}
[SignalR(TestSignalRTags.MultipleTypesParams)]
public string HandleMultipleTypes(bool flag, string text, int number)
{
return $"{flag}-{text}-{number}";
}
[SignalR(TestSignalRTags.ThrowsException)]
public void HandleThrowsException()
{
throw new InvalidOperationException("Test exception");
}
[SignalR(TestSignalRTags.DecimalParam)]
public decimal HandleDecimal(decimal value)
{
return value * 2;
}
[SignalR(TestSignalRTags.DateTimeParam)]
public DateTime HandleDateTime(DateTime dateTime)
{
return dateTime;
}
[SignalR(TestSignalRTags.DoubleParam)]
public double HandleDouble(double value)
{
return value;
}
[SignalR(TestSignalRTags.LongParam)]
public long HandleLong(long value)
{
return value;
}
#endregion
#region Complex Object Handlers (using shared DTOs)
[SignalR(TestSignalRTags.TestOrderItemParam)]
public TestOrderItem HandleTestOrderItem(TestOrderItem item)
{
return new TestOrderItem
{
Id = item.Id,
ProductName = $"Processed: {item.ProductName}",
Quantity = item.Quantity * 2,
UnitPrice = item.UnitPrice * 2,
};
}
[SignalR(TestSignalRTags.TestOrderParam)]
public TestOrder HandleTestOrder(TestOrder order)
{
return order;
}
[SignalR(TestSignalRTags.SharedTagParam)]
public SharedTag HandleSharedTag(SharedTag tag)
{
return tag;
}
#endregion
#region Collection Parameter Handlers
[SignalR(TestSignalRTags.IntArrayParam)]
public int[] HandleIntArray(int[] values)
{
return values.Select(x => x * 2).ToArray();
}
[SignalR(TestSignalRTags.GuidArrayParam)]
public Guid[] HandleGuidArray(Guid[] ids)
{
return ids;
}
[SignalR(TestSignalRTags.StringListParam)]
public List<string> HandleStringList(List<string> items)
{
return items.Select(x => x.ToUpper()).ToList();
}
[SignalR(TestSignalRTags.TestOrderItemListParam)]
public List<TestOrderItem> HandleTestOrderItemList(List<TestOrderItem> items)
{
return items;
}
[SignalR(TestSignalRTags.IntListParam)]
public List<int> HandleIntList(List<int> numbers)
{
return numbers.Select(x => x * 2).ToList();
}
[SignalR(TestSignalRTags.BoolArrayParam)]
public bool[] HandleBoolArray(bool[] flags)
{
return flags;
}
[SignalR(TestSignalRTags.MixedWithArrayParam)]
public string HandleMixedWithArray(bool flag, int[] numbers, string text)
{
return $"{flag}-[{string.Join(",", numbers)}]-{text}";
}
[SignalR(TestSignalRTags.NestedListParam)]
public List<List<int>> HandleNestedList(List<List<int>> nestedList)
{
return nestedList;
}
#endregion
#region Extended Array Parameter Handlers
[SignalR(TestSignalRTags.LongArrayParam)]
public long[] HandleLongArray(long[] values)
{
return values;
}
[SignalR(TestSignalRTags.DecimalArrayParam)]
public decimal[] HandleDecimalArray(decimal[] values)
{
return values;
}
[SignalR(TestSignalRTags.DateTimeArrayParam)]
public DateTime[] HandleDateTimeArray(DateTime[] values)
{
return values;
}
[SignalR(TestSignalRTags.EnumArrayParam)]
public TestStatus[] HandleEnumArray(TestStatus[] values)
{
return values;
}
[SignalR(TestSignalRTags.DoubleArrayParam)]
public double[] HandleDoubleArray(double[] values)
{
return values;
}
[SignalR(TestSignalRTags.SharedTagArrayParam)]
public SharedTag[] HandleSharedTagArray(SharedTag[] tags)
{
return tags;
}
[SignalR(TestSignalRTags.DictionaryParam)]
public Dictionary<string, int> HandleDictionary(Dictionary<string, int> dict)
{
return dict;
}
[SignalR(TestSignalRTags.ObjectArrayParam)]
public object[] HandleObjectArray(object[] values)
{
return values;
}
#endregion
#region Mixed Parameter Handlers
[SignalR(TestSignalRTags.IntAndDtoParam)]
public string HandleIntAndDto(int id, TestOrderItem item)
{
return $"{id}-{item?.ProductName}";
}
[SignalR(TestSignalRTags.DtoAndListParam)]
public string HandleDtoAndList(TestOrderItem item, List<int> numbers)
{
return $"{item?.ProductName}-[{string.Join(",", numbers ?? [])}]";
}
[SignalR(TestSignalRTags.ThreeComplexParams)]
public string HandleThreeComplexParams(TestOrderItem item, List<string> tags, SharedTag sharedTag)
{
return $"{item?.ProductName}-{tags?.Count}-{sharedTag?.Name}";
}
[SignalR(TestSignalRTags.FiveParams)]
public Task<string> HandleFiveParams(int a, string b, bool c, Guid d, decimal e)
{
return Task.FromResult($"{a}-{b}-{c}-{d}-{e.ToString(CultureInfo.InvariantCulture)}");
}
#endregion
#region Async Task<T> Method Tests
[SignalR(TestSignalRTags.AsyncTestOrderItemParam)]
public async Task<TestOrderItem> HandleAsyncTestOrderItem(TestOrderItem item)
{
await Task.Delay(1); // Simulate async work
return new TestOrderItem
{
Id = item.Id,
ProductName = $"Async: {item.ProductName}",
Quantity = item.Quantity * 3,
UnitPrice = item.UnitPrice * 3,
};
}
[SignalR(TestSignalRTags.AsyncStringParam)]
public async Task<string> HandleAsyncString(string input)
{
await Task.Delay(1);
return $"Async: {input}";
}
[SignalR(TestSignalRTags.AsyncNoParams)]
public async Task<string> HandleAsyncNoParams()
{
await Task.Delay(1);
return "AsyncOK";
}
[SignalR(TestSignalRTags.AsyncIntParam)]
public async Task<int> HandleAsyncInt(int value)
{
await Task.Delay(1);
return value * 2;
}
#endregion
#region Task.FromResult Tests - Critical for testing non-async methods returning Task
// PRODUCTION BUG FIX: These methods test the scenario where a method returns Task<T>
// using Task.FromResult() instead of async/await. Such methods do NOT have
// AsyncStateMachineAttribute, so the old InvokeMethod implementation would serialize
// the Task wrapper instead of awaiting and returning the actual result.
/// <summary>
/// Returns Task without async keyword - uses Task.FromResult().
/// This pattern does NOT have AsyncStateMachineAttribute!
/// </summary>
[SignalR(TestSignalRTags.TaskFromResultStringParam)]
public Task<string> HandleTaskFromResultString(string input)
{
return Task.FromResult($"FromResult: {input}");
}
/// <summary>
/// Returns Task&lt;TestOrderItem&gt; without async keyword.
/// CRITICAL: This simulates the exact production bug scenario.
/// </summary>
[SignalR(TestSignalRTags.TaskFromResultTestOrderItemParam)]
public Task<TestOrderItem> HandleTaskFromResultTestOrderItem(TestOrderItem item)
{
return Task.FromResult(new TestOrderItem
{
Id = item.Id,
ProductName = $"FromResult: {item.ProductName}",
Quantity = item.Quantity * 2,
UnitPrice = item.UnitPrice * 2,
});
}
/// <summary>
/// Returns Task&lt;int&gt; without async keyword.
/// </summary>
[SignalR(TestSignalRTags.TaskFromResultIntParam)]
public Task<int> HandleTaskFromResultInt(int value)
{
return Task.FromResult(value * 2);
}
/// <summary>
/// Returns non-generic Task using Task.CompletedTask.
/// </summary>
[SignalR(TestSignalRTags.TaskFromResultNoParams)]
public Task HandleTaskFromResultNoParams()
{
return Task.CompletedTask;
}
#endregion
#region Binary Serialization with GenericAttributes Test
/// <summary>
/// Tests Binary serialization with GenericAttributes containing string-stored DateTime values.
/// This reproduces the production bug scenario where DateTime values stored as strings
/// in GenericAttributes were incorrectly blamed for Binary serialization issues.
/// </summary>
[SignalR(TestSignalRTags.GenericAttributesParam)]
public TestDtoWithGenericAttributes HandleGenericAttributes(TestDtoWithGenericAttributes dto)
{
// Return the same DTO to verify Binary round-trip preserves all values
return dto;
}
/// <summary>
/// Tests Binary serialization with a list of DTOs containing GenericAttributes.
/// This simulates the production scenario with large datasets (e.g., 1834 orders).
/// </summary>
[SignalR(TestSignalRTags.GenericAttributesListParam)]
public List<TestDtoWithGenericAttributes> HandleGenericAttributesList(List<TestDtoWithGenericAttributes> dtos)
{
return dtos;
}
#endregion
#region Large Dataset / List Tests
/// <summary>
/// Tests Binary serialization with a list of TestOrder objects.
/// Used for testing string interning with deeply nested objects.
/// </summary>
[SignalR(TestSignalRTags.TestOrderListParam)]
public List<TestOrder> HandleTestOrderList(List<TestOrder> orders)
{
return orders;
}
#endregion
#region Property Mismatch Tests (Server has more properties than Client)
// Tests for SkipValue string interning bug fix.
// In these tests, the server sends DTOs with more properties than the client knows about.
// The client's deserializer must skip unknown properties while maintaining string intern table consistency.
/// <summary>
/// Handles server DTO and returns the same DTO.
/// Client will deserialize this into ClientCustomerDto (fewer properties).
/// </summary>
[SignalR(TestSignalRTags.PropertyMismatchParam)]
public ServerCustomerDto HandlePropertyMismatch(ServerCustomerDto dto)
{
return dto;
}
/// <summary>
/// Handles list of server DTOs.
/// Client will deserialize into List&lt;ClientCustomerDto&gt;.
/// </summary>
[SignalR(TestSignalRTags.PropertyMismatchListParam)]
public List<ServerCustomerDto> HandlePropertyMismatchList(List<ServerCustomerDto> dtos)
{
return dtos;
}
/// <summary>
/// Handles server order with nested customer objects.
/// Client will deserialize into ClientOrderSimple (no nested objects).
/// </summary>
[SignalR(TestSignalRTags.PropertyMismatchNestedParam)]
public ServerOrderWithExtras HandlePropertyMismatchNested(ServerOrderWithExtras order)
{
return order;
}
/// <summary>
/// Handles list of server orders with nested customer objects.
/// Client will deserialize into List&lt;ClientOrderSimple&gt;.
/// </summary>
[SignalR(TestSignalRTags.PropertyMismatchNestedListParam)]
public List<ServerOrderWithExtras> HandlePropertyMismatchNestedList(List<ServerOrderWithExtras> orders)
{
return orders;
}
#endregion
#region DataSource CRUD Tests
private readonly List<TestOrderItem> _dataSourceItems =
[
new() { Id = 1, ProductName = "Product A", Quantity = 10, UnitPrice = 100m },
new() { Id = 2, ProductName = "Product B", Quantity = 20, UnitPrice = 200m },
new() { Id = 3, ProductName = "Product C", Quantity = 30, UnitPrice = 300m }
];
[SignalR(TestSignalRTags.DataSourceGetAll)]
public List<TestOrderItem> DataSourceGetAll() => _dataSourceItems.ToList();
[SignalR(TestSignalRTags.DataSourceGetItem)]
public TestOrderItem? DataSourceGetItem(int id) => _dataSourceItems.FirstOrDefault(x => x.Id == id);
[SignalR(TestSignalRTags.DataSourceAdd)]
public TestOrderItem DataSourceAdd(TestOrderItem item)
{
_dataSourceItems.Add(item);
return item;
}
[SignalR(TestSignalRTags.DataSourceUpdate)]
public TestOrderItem DataSourceUpdate(TestOrderItem item)
{
var index = _dataSourceItems.FindIndex(x => x.Id == item.Id);
if (index >= 0) _dataSourceItems[index] = item;
return item;
}
[SignalR(TestSignalRTags.DataSourceRemove)]
public TestOrderItem? DataSourceRemove(TestOrderItem item)
{
var existing = _dataSourceItems.FirstOrDefault(x => x.Id == item.Id);
if (existing != null) _dataSourceItems.Remove(existing);
return existing;
}
#endregion
#region StockTaking Production Bug Reproduction
/// <summary>
/// Simulates the exact production scenario from FruitBank GetStockTakings(false).
/// Returns data from actual database records to reproduce the bug.
/// Uses the REAL StockTaking model from StockTakingTestModels.cs
/// </summary>
[SignalR(TestSignalRTags.GetStockTakings)]
public List<StockTaking> GetStockTakings(bool loadRelations)
{
// Exact data from production database:
return
[
new StockTaking
{
Id = 7,
StartDateTime = new DateTime(2025, 12, 3, 8, 55, 43, 539, DateTimeKind.Utc),
IsClosed = false, // This is the key - IsClosed=false gets skipped by serializer!
Creator = 6,
Created = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc),
Modified = new DateTime(2025, 12, 3, 7, 55, 43, 571, DateTimeKind.Utc),
StockTakingItems = loadRelations ? [] : null
},
new StockTaking
{
Id = 6,
StartDateTime = new DateTime(2025, 12, 2, 8, 21, 26, 439, DateTimeKind.Utc),
IsClosed = true,
Creator = 6,
Created = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc),
Modified = new DateTime(2025, 12, 2, 7, 21, 26, 468, DateTimeKind.Utc),
StockTakingItems = loadRelations ? [] : null
},
new StockTaking
{
Id = 3,
StartDateTime = new DateTime(2025, 11, 30, 14, 1, 55, 663, DateTimeKind.Utc),
IsClosed = true,
Creator = 6,
Created = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc),
Modified = new DateTime(2025, 11, 30, 13, 1, 55, 692, DateTimeKind.Utc),
StockTakingItems = loadRelations ? [] : null
},
new StockTaking
{
Id = 2,
StartDateTime = new DateTime(2025, 11, 30, 8, 20, 2, 182, DateTimeKind.Utc),
IsClosed = true,
Creator = 6,
Created = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc),
Modified = new DateTime(2025, 11, 30, 7, 20, 3, 331, DateTimeKind.Utc),
StockTakingItems = loadRelations ? [] : null
},
new StockTaking
{
Id = 1,
StartDateTime = new DateTime(2025, 11, 30, 8, 18, 59, 693, DateTimeKind.Utc),
IsClosed = true,
Creator = 6,
Created = new DateTime(2025, 11, 30, 7, 19, 1, 849, DateTimeKind.Utc),
Modified = new DateTime(2025, 11, 30, 7, 19, 1, 877, DateTimeKind.Utc),
StockTakingItems = loadRelations ? [] : null
}
];
}
#endregion
#region Default Parameter Tests
/// <summary>
/// Method with two parameters where the second has a default value.
/// Tests if the SignalR infrastructure correctly handles optional parameters.
/// </summary>
[SignalR(TestSignalRTags.TwoParamsWithDefault)]
public string HandleTwoParamsWithDefault(int requiredParam, bool optionalParam = true)
{
return $"Required: {requiredParam}, Optional: {optionalParam}";
}
#endregion
}