542 lines
16 KiB
C#
542 lines
16 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<TestOrderItem> 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<int> 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<ClientCustomerDto>.
|
|
/// </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<ClientOrderSimple>.
|
|
/// </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
|
|
}
|