diff --git a/AyCode.Core/Compression/BrotliHelper.cs b/AyCode.Core/Compression/BrotliHelper.cs
new file mode 100644
index 0000000..3739023
--- /dev/null
+++ b/AyCode.Core/Compression/BrotliHelper.cs
@@ -0,0 +1,114 @@
+using System.IO.Compression;
+using System.Text;
+
+namespace AyCode.Core.Compression;
+
+///
+/// Brotli compression/decompression helper for SignalR message transport.
+/// Used when JSON serializer is configured to reduce payload size.
+///
+public static class BrotliHelper
+{
+ ///
+ /// Compresses a string using Brotli compression.
+ ///
+ /// The text to compress.
+ /// Compression level (default: Optimal).
+ /// Compressed byte array.
+ public static byte[] Compress(string text, CompressionLevel compressionLevel = CompressionLevel.Optimal)
+ {
+ if (string.IsNullOrEmpty(text))
+ return [];
+
+ var bytes = Encoding.UTF8.GetBytes(text);
+ return Compress(bytes, compressionLevel);
+ }
+
+ ///
+ /// Compresses a byte array using Brotli compression.
+ ///
+ /// The data to compress.
+ /// Compression level (default: Optimal).
+ /// Compressed byte array.
+ public static byte[] Compress(byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal)
+ {
+ if (data == null || data.Length == 0)
+ return [];
+
+ using var outputStream = new MemoryStream();
+ using (var brotliStream = new BrotliStream(outputStream, compressionLevel, leaveOpen: true))
+ {
+ brotliStream.Write(data, 0, data.Length);
+ }
+
+ return outputStream.ToArray();
+ }
+
+ ///
+ /// Decompresses Brotli-compressed data to a string.
+ ///
+ /// The compressed data.
+ /// Decompressed string.
+ public static string DecompressToString(byte[] compressedData)
+ {
+ if (compressedData == null || compressedData.Length == 0)
+ return string.Empty;
+
+ var decompressedBytes = Decompress(compressedData);
+ return Encoding.UTF8.GetString(decompressedBytes);
+ }
+
+ ///
+ /// Decompresses Brotli-compressed data to a byte array.
+ ///
+ /// The compressed data.
+ /// Decompressed byte array.
+ public static byte[] Decompress(byte[] compressedData)
+ {
+ if (compressedData == null || compressedData.Length == 0)
+ return [];
+
+ using var inputStream = new MemoryStream(compressedData);
+ using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
+ using var outputStream = new MemoryStream();
+
+ brotliStream.CopyTo(outputStream);
+ return outputStream.ToArray();
+ }
+
+ ///
+ /// Checks if the data appears to be Brotli compressed.
+ /// Brotli doesn't have a magic number, so we use a heuristic approach.
+ ///
+ /// The data to check.
+ /// True if the data might be Brotli compressed.
+ public static bool IsBrotliCompressed(byte[] data)
+ {
+ if (data == null || data.Length < 4)
+ return false;
+
+ // Brotli doesn't have a magic header like gzip (0x1F 0x8B)
+ // We check if it's NOT valid UTF-8 JSON (starts with { or [)
+ // and try to decompress
+ var firstByte = data[0];
+
+ // If it starts with '{' (0x7B) or '[' (0x5B), it's likely uncompressed JSON
+ if (firstByte == 0x7B || firstByte == 0x5B)
+ return false;
+
+ // Try to decompress - if it fails, it's not Brotli
+ try
+ {
+ using var inputStream = new MemoryStream(data);
+ using var brotliStream = new BrotliStream(inputStream, CompressionMode.Decompress);
+
+ // Try to read first byte
+ var buffer = new byte[1];
+ return brotliStream.Read(buffer, 0, 1) >= 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Extensions/AcJsonDeserializer.cs
index 5d5b591..e16d203 100644
--- a/AyCode.Core/Extensions/AcJsonDeserializer.cs
+++ b/AyCode.Core/Extensions/AcJsonDeserializer.cs
@@ -1323,12 +1323,24 @@ public static class AcJsonDeserializer
case TypeCode.Decimal: result = decimal.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Single: result = float.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.String:
+ // If already unwrapped (no quotes), return as-is; otherwise parse JSON
+ if (json.Length == 0 || json[0] != '"')
+ {
+ result = json;
+ return true;
+ }
using (var doc = JsonDocument.Parse(json))
{
result = doc.RootElement.GetString();
return true;
}
case TypeCode.DateTime:
+ // If already unwrapped (no quotes), parse directly; otherwise use JSON parser
+ if (json.Length == 0 || json[0] != '"')
+ {
+ result = DateTime.Parse(json, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
+ return true;
+ }
using (var doc = JsonDocument.Parse(json))
{
result = doc.RootElement.GetDateTime();
@@ -1341,6 +1353,11 @@ public static class AcJsonDeserializer
case TypeCode.UInt64: result = ulong.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.SByte: result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Char:
+ if (json.Length == 0 || json[0] != '"')
+ {
+ result = json.Length > 0 ? json[0] : '\0';
+ return true;
+ }
using (var doc = JsonDocument.Parse(json))
{
var s = doc.RootElement.GetString();
@@ -1351,6 +1368,12 @@ public static class AcJsonDeserializer
if (ReferenceEquals(type, GuidType))
{
+ // If already unwrapped (no quotes), parse directly
+ if (json.Length == 0 || json[0] != '"')
+ {
+ result = Guid.Parse(json);
+ return true;
+ }
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetGuid();
return true;
@@ -1358,6 +1381,12 @@ public static class AcJsonDeserializer
if (ReferenceEquals(type, DateTimeOffsetType))
{
+ // If already unwrapped (no quotes), parse directly
+ if (json.Length == 0 || json[0] != '"')
+ {
+ result = DateTimeOffset.Parse(json, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
+ return true;
+ }
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetDateTimeOffset();
return true;
@@ -1365,6 +1394,12 @@ public static class AcJsonDeserializer
if (ReferenceEquals(type, TimeSpanType))
{
+ // If already unwrapped (no quotes), parse directly
+ if (json.Length == 0 || json[0] != '"')
+ {
+ result = TimeSpan.Parse(json, CultureInfo.InvariantCulture);
+ return true;
+ }
using var doc = JsonDocument.Parse(json);
result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture);
return true;
diff --git a/AyCode.Core/Extensions/SerializeObjectExtensions.cs b/AyCode.Core/Extensions/SerializeObjectExtensions.cs
index cae3976..3be6eee 100644
--- a/AyCode.Core/Extensions/SerializeObjectExtensions.cs
+++ b/AyCode.Core/Extensions/SerializeObjectExtensions.cs
@@ -1,4 +1,5 @@
-using System.Collections.Concurrent;
+using System.Buffers;
+using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
@@ -337,6 +338,8 @@ public static class SerializeObjectExtensions
Formatting = Formatting.None,
};
+ #region JSON Serialization
+
///
/// Serialize object to JSON string with default options.
///
@@ -414,29 +417,9 @@ public static class SerializeObjectExtensions
AcJsonDeserializer.Populate(json, target, options);
}
- ///
- /// Clone object via JSON serialization with default options.
- ///
- public static TDestination? CloneTo(this object? src) where TDestination : class
- => src?.ToJson().JsonTo();
+ #endregion
- ///
- /// Clone object via JSON serialization with specified options.
- ///
- public static TDestination? CloneTo(this object? src, AcJsonSerializerOptions options) where TDestination : class
- => src?.ToJson(options).JsonTo(options);
-
- ///
- /// Copy object properties to target via JSON with default options.
- ///
- public static void CopyTo(this object? src, object target)
- => src?.ToJson().JsonTo(target);
-
- ///
- /// Copy object properties to target via JSON with specified options.
- ///
- public static void CopyTo(this object? src, object target, AcJsonSerializerOptions options)
- => src?.ToJson(options).JsonTo(target, options);
+ #region MessagePack
public static byte[] ToMessagePack(this object message, MessagePackSerializerOptions options)
=> MessagePackSerializer.Serialize(message, options);
@@ -444,6 +427,9 @@ public static class SerializeObjectExtensions
public static T MessagePackTo(this byte[] message, MessagePackSerializerOptions options)
=> MessagePackSerializer.Deserialize(message, options);
+ #endregion
+
+ #region Any (JSON or Binary based on options)
public static object ToAny(this T source, AcSerializerOptions options)
{
@@ -488,37 +474,17 @@ public static class SerializeObjectExtensions
public static void AnyToMerge(this object data, T target, AcSerializerOptions options) where T : class
{
if (options.SerializerType == AcSerializerType.Json)
- ((string)data).JsonTo(target, (AcJsonSerializerOptions)options); // JSON always merges
+ ((string)data).JsonTo(target, (AcJsonSerializerOptions)options);
else
((byte[])data).BinaryToMerge(target);
}
- ///
- /// Clone object via serialization based on options.
- ///
- public static T? CloneToAny(this T source, AcSerializerOptions options) where T : class
- {
- if (options.SerializerType == AcSerializerType.Json)
- return source.CloneTo((AcJsonSerializerOptions)options);
- return source.BinaryCloneTo();
- }
+ #endregion
- ///
- /// Copy object properties to target via serialization based on options.
- ///
- public static void CopyToAny(this T source, T target, AcSerializerOptions options) where T : class
- {
- if (options.SerializerType == AcSerializerType.Json)
- source.CopyTo(target, (AcJsonSerializerOptions)options);
- else
- source.BinaryCopyTo(target);
- }
-
- #region Binary Serialization Extension Methods
+ #region Binary Serialization
///
/// Serialize object to binary byte array with default options.
- /// Significantly faster than JSON, especially for large data in WASM.
///
public static byte[] ToBinary(this T source) => AcBinarySerializer.Serialize(source);
@@ -529,29 +495,83 @@ public static class SerializeObjectExtensions
=> AcBinarySerializer.Serialize(source, options);
///
- /// Deserialize binary data to object with default options.
+ /// Serialize object directly to an IBufferWriter for zero-copy scenarios.
+ ///
+ public static void ToBinary(this T source, IBufferWriter writer)
+ => AcBinarySerializer.Serialize(source, writer, AcBinarySerializerOptions.Default);
+
+ ///
+ /// Serialize object directly to an IBufferWriter with specified options.
+ ///
+ public static void ToBinary(this T source, IBufferWriter writer, AcBinarySerializerOptions options)
+ => AcBinarySerializer.Serialize(source, writer, options);
+
+ ///
+ /// Get the serialized binary size without allocating the final array.
+ ///
+ public static int GetBinarySize(this T source)
+ => AcBinarySerializer.GetSerializedSize(source, AcBinarySerializerOptions.Default);
+
+ ///
+ /// Get the serialized binary size with specified options.
+ ///
+ public static int GetBinarySize(this T source, AcBinarySerializerOptions options)
+ => AcBinarySerializer.GetSerializedSize(source, options);
+
+ ///
+ /// Deserialize binary data to object.
///
public static T? BinaryTo(this byte[] data)
=> AcBinaryDeserializer.Deserialize(data);
///
- /// Deserialize binary data to object.
+ /// Deserialize binary data from ReadOnlySpan.
///
public static T? BinaryTo(this ReadOnlySpan data)
=> AcBinaryDeserializer.Deserialize(data);
+ ///
+ /// Deserialize binary data from ReadOnlyMemory.
+ ///
+ public static T? BinaryTo(this ReadOnlyMemory data)
+ => AcBinaryDeserializer.Deserialize(data.Span);
+
///
/// Deserialize binary data to specified type.
///
public static object? BinaryTo(this byte[] data, Type targetType)
=> AcBinaryDeserializer.Deserialize(data.AsSpan(), targetType);
+ ///
+ /// Deserialize binary data from ReadOnlySpan to specified type.
+ ///
+ public static object? BinaryTo(this ReadOnlySpan data, Type targetType)
+ => AcBinaryDeserializer.Deserialize(data, targetType);
+
+ ///
+ /// Deserialize binary data from ReadOnlyMemory to specified type.
+ ///
+ public static object? BinaryTo(this ReadOnlyMemory data, Type targetType)
+ => AcBinaryDeserializer.Deserialize(data.Span, targetType);
+
///
/// Populate existing object from binary data.
///
public static void BinaryTo(this byte[] data, T target) where T : class
=> AcBinaryDeserializer.Populate(data, target);
+ ///
+ /// Populate existing object from binary ReadOnlySpan.
+ ///
+ public static void BinaryTo(this ReadOnlySpan data, T target) where T : class
+ => AcBinaryDeserializer.Populate(data, target);
+
+ ///
+ /// Populate existing object from binary ReadOnlyMemory.
+ ///
+ public static void BinaryTo(this ReadOnlyMemory data, T target) where T : class
+ => AcBinaryDeserializer.Populate(data.Span, target);
+
///
/// Populate existing object from binary data with merge semantics for IId collections.
///
@@ -559,16 +579,46 @@ public static class SerializeObjectExtensions
=> AcBinaryDeserializer.PopulateMerge(data.AsSpan(), target);
///
- /// Clone object via binary serialization (faster than JSON clone).
+ /// Populate existing object from binary ReadOnlySpan with merge semantics.
///
- public static T? BinaryCloneTo(this T source) where T : class
- => source?.ToBinary().BinaryTo();
+ public static void BinaryToMerge(this ReadOnlySpan data, T target) where T : class
+ => AcBinaryDeserializer.PopulateMerge(data, target);
///
- /// Copy object properties to target via binary serialization.
+ /// Populate existing object from binary ReadOnlyMemory with merge semantics.
///
- public static void BinaryCopyTo(this T source, T target) where T : class
- => source?.ToBinary().BinaryTo(target);
+ public static void BinaryToMerge(this ReadOnlyMemory data, T target) where T : class
+ => AcBinaryDeserializer.PopulateMerge(data.Span, target);
+
+ #endregion
+
+ #region Clone and Copy (Binary-based, zero intermediate allocation)
+
+ ///
+ /// Clone object via binary serialization (zero intermediate byte[] allocation).
+ /// Uses ArrayBufferWriter to serialize directly into a buffer, then deserializes from the span.
+ ///
+ public static TDestination? CloneTo(this object? src) where TDestination : class
+ {
+ if (src == null) return null;
+
+ var buffer = new ArrayBufferWriter(256);
+ AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default);
+ return AcBinaryDeserializer.Deserialize(buffer.WrittenSpan);
+ }
+
+ ///
+ /// Copy object properties to target via binary serialization (zero intermediate byte[] allocation).
+ /// Uses ArrayBufferWriter to serialize directly into a buffer, then populates target from the span.
+ ///
+ public static void CopyTo(this object? src, object target)
+ {
+ if (src == null) return;
+
+ var buffer = new ArrayBufferWriter(256);
+ AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default);
+ AcBinaryDeserializer.Populate(buffer.WrittenSpan, target);
+ }
#endregion
}
diff --git a/AyCode.Services.Server.Tests/SignalRs/AcSignalRDataSourceTests.cs b/AyCode.Services.Server.Tests/SignalRs/AcSignalRDataSourceTests.cs
deleted file mode 100644
index 56e8ef2..0000000
--- a/AyCode.Services.Server.Tests/SignalRs/AcSignalRDataSourceTests.cs
+++ /dev/null
@@ -1,1301 +0,0 @@
-using AyCode.Core.Enums;
-using AyCode.Core.Extensions;
-using AyCode.Core.Helpers;
-using AyCode.Core.Interfaces;
-using AyCode.Core.Loggers;
-using AyCode.Services.SignalRs;
-using AyCode.Services.Server.SignalRs;
-using System.Collections.Concurrent;
-using System.Runtime.CompilerServices;
-
-namespace AyCode.Services.Server.Tests.SignalRs;
-
-#region Test Models
-
-public class TestDataItem : IId
-{
- public int Id { get; set; }
- public string Name { get; set; } = string.Empty;
- public int Value { get; set; }
-
- public TestDataItem() { }
- public TestDataItem(int id, string name, int value = 0)
- {
- Id = id;
- Name = name;
- Value = value;
- }
-
- public override string ToString() => $"TestDataItem[{Id}, {Name}, {Value}]";
-}
-
-public class TestDataSource : AcSignalRDataSource>
-{
- public TestDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
- : base(signalRClient, signalRCrudTags, contextIds)
- {
- }
-}
-
-public class TestObservableDataSource : AcSignalRDataSource>
-{
- public TestObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
- : base(signalRClient, signalRCrudTags, contextIds)
- {
- }
-}
-
-#endregion
-
-#region Mock SignalR Client
-
-///
-/// Mock SignalR client for testing AcSignalRDataSource without actual network calls.
-/// Uses the test constructor to avoid real HubConnection.
-///
-public class MockSignalRClient : AcSignalRClientBase
-{
- private readonly ConcurrentDictionary> _responseHandlers = new();
- private readonly ConcurrentBag<(int MessageTag, object? Data, DateTime Timestamp)> _sentMessages = new();
- private static int _idCounter;
-
- public IReadOnlyList<(int MessageTag, object? Data, DateTime Timestamp)> SentMessages
- => _sentMessages.OrderBy(x => x.Timestamp).ToList();
-
- public int GetSentMessageCount(int messageTag)
- => _sentMessages.Count(m => m.MessageTag == messageTag);
-
- public static int NextId() => Interlocked.Increment(ref _idCounter);
- public static void ResetIdCounter() => _idCounter = 0;
-
- ///
- /// Uses test constructor - no real HubConnection created.
- ///
- public MockSignalRClient() : base(new MockLogger())
- {
- }
-
- ///
- /// Setup a static response for a specific message tag
- ///
- public void SetupResponse(int messageTag, TResponse response)
- {
- _responseHandlers[messageTag] = _ => response;
- }
-
- ///
- /// Setup a dynamic response based on the request
- ///
- public void SetupResponse(int messageTag, Func responseFactory)
- {
- _responseHandlers[messageTag] = req => responseFactory((TRequest?)req);
- }
-
- ///
- /// Setup a response that returns the posted data (echo)
- ///
- public void SetupEchoResponse(int messageTag) where T : class
- {
- _responseHandlers[messageTag] = req => req;
- }
-
- ///
- /// Clear all response handlers
- ///
- public void ClearResponses()
- {
- _responseHandlers.Clear();
- }
-
- public override Task GetAllAsync(int messageTag, object[]? contextParams) where TResponseData : default
- {
- _sentMessages.Add((messageTag, contextParams, DateTime.UtcNow));
-
- if (_responseHandlers.TryGetValue(messageTag, out var handler))
- {
- return Task.FromResult((TResponseData?)handler(contextParams));
- }
- return Task.FromResult(default(TResponseData));
- }
-
- public override Task GetAllAsync(int messageTag, Func, Task> responseCallback, object[]? contextParams) where TResponseData : default
- {
- _sentMessages.Add((messageTag, contextParams, DateTime.UtcNow));
-
- if (_responseHandlers.TryGetValue(messageTag, out var handler))
- {
- var response = (TResponseData?)handler(contextParams);
- var responseJson = response?.ToJson();
- ISignalResponseMessage message = new SignalResponseMessage(messageTag, SignalResponseStatus.Success, responseJson);
- return responseCallback(message);
- }
-
- ISignalResponseMessage errorMessage = new SignalResponseMessage(messageTag, SignalResponseStatus.Error, (string?)null);
- return responseCallback(errorMessage);
- }
-
- public override Task GetByIdAsync(int messageTag, object id) where TResponseData : default
- {
- _sentMessages.Add((messageTag, id, DateTime.UtcNow));
-
- if (_responseHandlers.TryGetValue(messageTag, out var handler))
- {
- return Task.FromResult((TResponseData?)handler(id));
- }
- return Task.FromResult(default(TResponseData));
- }
-
- public override Task PostDataAsync(int messageTag, TPostData postData) where TPostData : class
- {
- _sentMessages.Add((messageTag, postData, DateTime.UtcNow));
-
- if (_responseHandlers.TryGetValue(messageTag, out var handler))
- {
- return Task.FromResult((TPostData?)handler(postData));
- }
- // Default: echo back the posted data
- return Task.FromResult(postData);
- }
-
- public override Task PostDataAsync(int messageTag, TPostData postData, Func, Task> responseCallback) where TPostData : default
- {
- _sentMessages.Add((messageTag, postData, DateTime.UtcNow));
-
- if (_responseHandlers.TryGetValue(messageTag, out var handler))
- {
- var response = (TPostData?)handler(postData);
- var responseJson = response?.ToJson();
- ISignalResponseMessage message = new SignalResponseMessage(messageTag, SignalResponseStatus.Success, responseJson);
- return responseCallback(message);
- }
-
- // Default: echo back the posted data
- ISignalResponseMessage successMessage = new SignalResponseMessage(messageTag, SignalResponseStatus.Success, postData?.ToJson());
- return responseCallback(successMessage);
- }
-
- protected override Task MessageReceived(int messageTag, byte[] messageBytes)
- {
- return Task.CompletedTask;
- }
-}
-
-///
-/// Silent logger for testing - does not require appsettings.json
-///
-public class MockLogger : AcLoggerBase
-{
- private readonly List _logs = new();
- public IReadOnlyList Logs => _logs;
-
- public MockLogger() : base(AppType.TestUnit, LogLevel.Error, "MockLogger")
- {
- }
-
- public override void Detail(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
- => _logs.Add($"DETAIL: {text}");
-
- public override void Debug(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
- => _logs.Add($"DEBUG: {text}");
-
- public override void Info(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
- => _logs.Add($"INFO: {text}");
-
- public override void Warning(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
- => _logs.Add($"WARN: {text}");
-
- public override void Error(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
- => _logs.Add($"ERROR: {text}");
-}
-
-#endregion
-
-[TestClass]
-public class AcSignalRDataSourceTests
-{
- private const int GetAllTag = 100;
- private const int GetItemTag = 101;
- private const int AddTag = 102;
- private const int UpdateTag = 103;
- private const int RemoveTag = 104;
-
- private MockSignalRClient _mockClient = null!;
- private SignalRCrudTags _crudTags = null!;
- private TestDataSource _dataSource = null!;
-
- [TestInitialize]
- public void Setup()
- {
- MockSignalRClient.ResetIdCounter();
- _mockClient = new MockSignalRClient();
- _crudTags = new SignalRCrudTags(GetAllTag, GetItemTag, AddTag, UpdateTag, RemoveTag);
- _dataSource = new TestDataSource(_mockClient, _crudTags);
- }
-
- #region Basic CRUD Tests
-
- [TestMethod]
- public void Add_ValidItem_AddsToCollection()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
-
- // Act
- _dataSource.Add(item);
-
- // Assert
- Assert.AreEqual(1, _dataSource.Count);
- Assert.IsTrue(_dataSource.Contains(item));
- }
-
- [TestMethod]
- public void Add_ItemWithDefaultId_ThrowsArgumentNullException()
- {
- // Arrange
- var item = new TestDataItem(0, "Invalid Item"); // 0 is default for int
-
- // Act & Assert
- try
- {
- _dataSource.Add(item);
- Assert.Fail("Expected ArgumentNullException was not thrown");
- }
- catch (ArgumentNullException)
- {
- // Expected
- }
- }
-
- [TestMethod]
- public void Add_DuplicateItem_ThrowsArgumentException()
- {
- // Arrange
- var id = MockSignalRClient.NextId();
- var item1 = new TestDataItem(id, "Item 1");
- var item2 = new TestDataItem(id, "Item 2");
- _dataSource.Add(item1);
-
- // Act & Assert
- try
- {
- _dataSource.Add(item2);
- Assert.Fail("Expected ArgumentException was not thrown");
- }
- catch (ArgumentException)
- {
- // Expected
- }
- }
-
- [TestMethod]
- public void Remove_ExistingItem_RemovesFromCollection()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource.Remove(item);
-
- // Assert
- Assert.IsTrue(result);
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- [TestMethod]
- public void Remove_NonExistingItem_ReturnsFalse()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
-
- // Act
- var result = _dataSource.Remove(item);
-
- // Assert
- Assert.IsFalse(result);
- }
-
- [TestMethod]
- public void Indexer_ValidIndex_ReturnsItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource[0];
-
- // Assert
- Assert.AreEqual(item.Id, result.Id);
- }
-
- [TestMethod]
- public void Indexer_InvalidIndex_ThrowsArgumentOutOfRangeException()
- {
- // Act & Assert
- try
- {
- var _ = _dataSource[0];
- Assert.Fail("Expected ArgumentOutOfRangeException was not thrown");
- }
- catch (ArgumentOutOfRangeException)
- {
- // Expected
- }
- }
-
- [TestMethod]
- public void Insert_ValidIndexAndItem_InsertsAtCorrectPosition()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- var item3 = new TestDataItem(MockSignalRClient.NextId(), "Item 3");
-
- _dataSource.Add(item1);
- _dataSource.Add(item3);
-
- // Act
- _dataSource.Insert(1, item2);
-
- // Assert
- Assert.AreEqual(3, _dataSource.Count);
- Assert.AreEqual(item2.Id, _dataSource[1].Id);
- }
-
- [TestMethod]
- public void Clear_WithItems_RemovesAllItems()
- {
- // Arrange
- _dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 1"));
- _dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 2"));
-
- // Act
- _dataSource.Clear();
-
- // Assert
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- [TestMethod]
- public void TryGetValue_ExistingId_ReturnsTrue()
- {
- // Arrange
- var id = MockSignalRClient.NextId();
- var item = new TestDataItem(id, "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource.TryGetValue(id, out var foundItem);
-
- // Assert
- Assert.IsTrue(result);
- Assert.IsNotNull(foundItem);
- Assert.AreEqual(id, foundItem.Id);
- }
-
- [TestMethod]
- public void TryGetValue_NonExistingId_ReturnsFalse()
- {
- // Act
- var result = _dataSource.TryGetValue(999, out var foundItem);
-
- // Assert
- Assert.IsFalse(result);
- Assert.IsNull(foundItem);
- }
-
- [TestMethod]
- public void RemoveAt_ValidIndex_RemovesItem()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- _dataSource.Add(item1);
- _dataSource.Add(item2);
-
- // Act
- _dataSource.RemoveAt(0);
-
- // Assert
- Assert.AreEqual(1, _dataSource.Count);
- Assert.AreEqual(item2.Id, _dataSource[0].Id);
- }
-
- #endregion
-
- #region Tracking Tests
-
- [TestMethod]
- public void Add_CreatesTrackingItem_WithAddState()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
-
- // Act
- _dataSource.Add(item);
-
- // Assert
- var trackingItems = _dataSource.GetTrackingItems();
- Assert.AreEqual(1, trackingItems.Count);
- Assert.AreEqual(TrackingState.Add, trackingItems[0].TrackingState);
- Assert.AreEqual(item.Id, trackingItems[0].CurrentValue.Id);
- }
-
- [TestMethod]
- public void Remove_AfterAdd_RemovesTrackingItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- _dataSource.Remove(item);
-
- // Assert - Tracking should be empty because Add followed by Remove cancels out
- var trackingItems = _dataSource.GetTrackingItems();
- Assert.AreEqual(0, trackingItems.Count);
- }
-
- [TestMethod]
- public void TryGetTrackingItem_ExistingItem_ReturnsTrue()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource.TryGetTrackingItem(item.Id, out var trackingItem);
-
- // Assert
- Assert.IsTrue(result);
- Assert.IsNotNull(trackingItem);
- Assert.AreEqual(TrackingState.Add, trackingItem.TrackingState);
- }
-
- [TestMethod]
- public void SetTrackingStateToUpdate_AddsUpdateTracking()
- {
- // Arrange - Load data without tracking
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item", 1);
- var innerList = _dataSource.GetReferenceInnerList();
- innerList.Add(item);
-
- // Act
- _dataSource.SetTrackingStateToUpdate(item);
-
- // Assert
- Assert.IsTrue(_dataSource.TryGetTrackingItem(item.Id, out var trackingItem));
- Assert.AreEqual(TrackingState.Update, trackingItem!.TrackingState);
- }
-
- [TestMethod]
- public void SetTrackingStateToUpdate_DoesNotChangeAddState()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- _dataSource.SetTrackingStateToUpdate(item);
-
- // Assert - Should still be Add, not Update
- Assert.IsTrue(_dataSource.TryGetTrackingItem(item.Id, out var trackingItem));
- Assert.AreEqual(TrackingState.Add, trackingItem!.TrackingState);
- }
-
- [TestMethod]
- public void TryRollbackItem_RollsBackAddedItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource.TryRollbackItem(item.Id, out var originalValue);
-
- // Assert
- Assert.IsTrue(result);
- Assert.IsNull(originalValue); // Added items have no original value
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- [TestMethod]
- public void Rollback_RollsBackAllChanges()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- _dataSource.Add(item1);
- _dataSource.Add(item2);
-
- // Act
- _dataSource.Rollback();
-
- // Assert
- Assert.AreEqual(0, _dataSource.Count);
- Assert.AreEqual(0, _dataSource.GetTrackingItems().Count);
- }
-
- [TestMethod]
- public void Clear_WithClearChangeTrackingFalse_KeepsTrackingItems()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- _dataSource.Clear(clearChangeTracking: false);
-
- // Assert
- Assert.AreEqual(0, _dataSource.Count);
- Assert.AreEqual(1, _dataSource.GetTrackingItems().Count);
- }
-
- [TestMethod]
- public void Clear_WithClearChangeTrackingTrue_RemovesTrackingItems()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- _dataSource.Clear(clearChangeTracking: true);
-
- // Assert
- Assert.AreEqual(0, _dataSource.Count);
- Assert.AreEqual(0, _dataSource.GetTrackingItems().Count);
- }
-
- #endregion
-
- #region Async Save Tests
-
- [TestMethod]
- public async Task Add_WithAutoSave_CallsSignalRClient()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _mockClient.SetupEchoResponse(AddTag);
-
- // Act
- var result = await _dataSource.Add(item, autoSave: true);
-
- // Assert
- Assert.IsNotNull(result);
- Assert.AreEqual(item.Id, result.Id);
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(AddTag));
- }
-
- [TestMethod]
- public async Task Add_WithAutoSaveFalse_DoesNotCallSignalR()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
-
- // Act
- var result = await _dataSource.Add(item, autoSave: false);
-
- // Assert
- Assert.IsNotNull(result);
- Assert.AreEqual(0, _mockClient.GetSentMessageCount(AddTag));
- }
-
- [TestMethod]
- public async Task Remove_WithAutoSave_CallsSignalRClient()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
- _mockClient.SetupEchoResponse(RemoveTag);
-
- // Act
- var result = await _dataSource.Remove(item, autoSave: true);
-
- // Assert
- Assert.IsTrue(result);
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(RemoveTag));
- }
-
- [TestMethod]
- public async Task Update_WithAutoSave_CallsSignalRClient()
- {
- // Arrange
- var id = MockSignalRClient.NextId();
- var item = new TestDataItem(id, "Test Item", 1);
- _dataSource.Add(item);
-
- var updatedItem = new TestDataItem(id, "Updated Item", 2);
- _mockClient.SetupEchoResponse(UpdateTag);
-
- // Act
- var result = await _dataSource.Update(updatedItem, autoSave: true);
-
- // Assert
- Assert.IsNotNull(result);
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(UpdateTag));
- }
-
- [TestMethod]
- public async Task AddOrUpdate_NewItem_AddsItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "New Item");
- _mockClient.SetupEchoResponse(AddTag);
-
- // Act
- var result = await _dataSource.AddOrUpdate(item, autoSave: true);
-
- // Assert
- Assert.AreEqual(1, _dataSource.Count);
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(AddTag));
- Assert.AreEqual(0, _mockClient.GetSentMessageCount(UpdateTag));
- }
-
- [TestMethod]
- public async Task AddOrUpdate_ExistingItem_UpdatesItem()
- {
- // Arrange
- var id = MockSignalRClient.NextId();
- var item = new TestDataItem(id, "Original", 1);
- _dataSource.Add(item);
-
- var updatedItem = new TestDataItem(id, "Updated", 2);
- _mockClient.SetupEchoResponse(UpdateTag);
-
- // Act
- var result = await _dataSource.AddOrUpdate(updatedItem, autoSave: true);
-
- // Assert
- Assert.AreEqual(1, _dataSource.Count);
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(UpdateTag));
- }
-
- [TestMethod]
- public async Task SaveChanges_SavesAllTrackedItems()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- _dataSource.Add(item1);
- _dataSource.Add(item2);
-
- _mockClient.SetupEchoResponse(AddTag);
-
- // Act
- var unsavedItems = await _dataSource.SaveChanges();
-
- // Assert
- Assert.AreEqual(0, unsavedItems.Count);
- Assert.AreEqual(2, _mockClient.GetSentMessageCount(AddTag));
- }
-
- [TestMethod]
- public async Task SaveChangesAsync_SavesAllTrackedItems()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- _dataSource.Add(item1);
- _dataSource.Add(item2);
-
- _mockClient.SetupEchoResponse(AddTag);
-
- // Act
- await _dataSource.SaveChangesAsync();
-
- // Assert
- Assert.AreEqual(0, _dataSource.GetTrackingItems().Count);
- }
-
- #endregion
-
- #region Load Tests
-
- [TestMethod]
- public async Task LoadDataSource_LoadsItemsFromSignalR()
- {
- // Arrange
- var items = new List
- {
- new(MockSignalRClient.NextId(), "Item 1"),
- new(MockSignalRClient.NextId(), "Item 2"),
- new(MockSignalRClient.NextId(), "Item 3")
- };
- _mockClient.SetupResponse(GetAllTag, items);
-
- // Act
- await _dataSource.LoadDataSource();
-
- // Assert
- Assert.AreEqual(3, _dataSource.Count);
- Assert.AreEqual(0, _dataSource.GetTrackingItems().Count); // No tracking for loaded items
- }
-
- [TestMethod]
- public async Task LoadItem_LoadsSingleItemFromSignalR()
- {
- // Arrange
- var id = MockSignalRClient.NextId();
- var item = new TestDataItem(id, "Loaded Item");
- _mockClient.SetupResponse(GetItemTag, item);
-
- // Act
- var result = await _dataSource.LoadItem(id);
-
- // Assert
- Assert.IsNotNull(result);
- Assert.AreEqual(id, result.Id);
- Assert.AreEqual(1, _dataSource.Count);
- }
-
- [TestMethod]
- public async Task LoadItem_ReturnsNullForNonExisting()
- {
- // Arrange
- _mockClient.SetupResponse(GetItemTag, null);
-
- // Act
- var result = await _dataSource.LoadItem(999);
-
- // Assert
- Assert.IsNull(result);
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- [TestMethod]
- public async Task LoadDataSource_FromList_CopiesItems()
- {
- // Arrange
- var sourceList = new List
- {
- new(MockSignalRClient.NextId(), "Item 1"),
- new(MockSignalRClient.NextId(), "Item 2")
- };
-
- // Act
- await _dataSource.LoadDataSource(sourceList);
-
- // Assert
- Assert.AreEqual(2, _dataSource.Count);
- }
-
- #endregion
-
- #region Thread Safety Tests
-
- [TestMethod]
- public async Task ConcurrentAdds_AreThreadSafe()
- {
- // Arrange
- var tasks = new List();
- var itemCount = 100;
-
- // Act
- for (int i = 0; i < itemCount; i++)
- {
- var item = new TestDataItem(MockSignalRClient.NextId(), $"Item {i}");
- tasks.Add(Task.Run(() => _dataSource.Add(item)));
- }
-
- await Task.WhenAll(tasks);
-
- // Assert
- Assert.AreEqual(itemCount, _dataSource.Count);
- }
-
- [TestMethod]
- public async Task ConcurrentReadsAndWrites_AreThreadSafe()
- {
- // Arrange - Pre-populate with items
- for (int i = 0; i < 50; i++)
- {
- _dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), $"Initial Item {i}"));
- }
-
- var tasks = new List();
- var readCount = 0;
- var writeCount = 0;
-
- // Act - Concurrent reads
- for (int i = 0; i < 100; i++)
- {
- tasks.Add(Task.Run(() =>
- {
- var count = _dataSource.Count;
- Interlocked.Increment(ref readCount);
- }));
- }
-
- // Act - Concurrent writes
- for (int i = 0; i < 50; i++)
- {
- var item = new TestDataItem(MockSignalRClient.NextId(), $"New Item {i}");
- tasks.Add(Task.Run(() =>
- {
- _dataSource.Add(item);
- Interlocked.Increment(ref writeCount);
- }));
- }
-
- await Task.WhenAll(tasks);
-
- // Assert
- Assert.AreEqual(100, readCount);
- Assert.AreEqual(50, writeCount);
- Assert.AreEqual(100, _dataSource.Count); // 50 original + 50 new
- }
-
- [TestMethod]
- public async Task ConcurrentAsyncOperations_AreThreadSafe()
- {
- // Arrange
- _mockClient.SetupEchoResponse(AddTag);
-
- var tasks = new List>();
-
- // Act
- for (int i = 0; i < 20; i++)
- {
- var item = new TestDataItem(MockSignalRClient.NextId(), $"Item {i}");
- tasks.Add(_dataSource.Add(item, autoSave: true));
- }
-
- var results = await Task.WhenAll(tasks);
-
- // Assert
- Assert.AreEqual(20, results.Length);
- Assert.AreEqual(20, _dataSource.Count);
- }
-
- [TestMethod]
- public void GetEnumerator_ReturnsCopy_SafeForModification()
- {
- // Arrange
- for (int i = 0; i < 10; i++)
- {
- _dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), $"Item {i}"));
- }
-
- // Act & Assert - Should not throw even when modifying during enumeration
- var enumeratedItems = new List();
- foreach (var item in _dataSource)
- {
- enumeratedItems.Add(item);
- if (enumeratedItems.Count == 5)
- {
- // This would throw if we're not using a copy
- _dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "New Item"));
- }
- }
-
- Assert.AreEqual(10, enumeratedItems.Count); // Original count
- Assert.AreEqual(11, _dataSource.Count); // After modification
- }
-
- [TestMethod]
- public async Task ConcurrentRemoves_AreThreadSafe()
- {
- // Arrange
- var items = Enumerable.Range(0, 50)
- .Select(i => new TestDataItem(MockSignalRClient.NextId(), $"Item {i}"))
- .ToList();
-
- foreach (var item in items)
- {
- _dataSource.Add(item);
- }
-
- // Act
- var tasks = items.Select(item => Task.Run(() => _dataSource.Remove(item))).ToList();
- await Task.WhenAll(tasks);
-
- // Assert
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- #endregion
-
- #region Observable Collection Tests
-
- [TestMethod]
- public void WithObservableCollection_BeginEndUpdate_SuppressesNotifications()
- {
- // Arrange
- var observableDataSource = new TestObservableDataSource(_mockClient, _crudTags);
- var innerList = observableDataSource.GetReferenceInnerList();
- var notificationCount = 0;
-
- innerList.CollectionChanged += (s, e) => notificationCount++;
-
- // Act
- innerList.BeginUpdate();
- for (int i = 0; i < 10; i++)
- {
- innerList.Add(new TestDataItem(MockSignalRClient.NextId(), $"Item {i}"));
- }
- innerList.EndUpdate();
-
- // Assert - Should only have 1 notification (Reset) instead of 10
- Assert.AreEqual(1, notificationCount);
- Assert.AreEqual(10, innerList.Count);
- }
-
- [TestMethod]
- public void WithObservableCollection_NestedUpdates_OnlyFiresOnce()
- {
- // Arrange
- var observableDataSource = new TestObservableDataSource(_mockClient, _crudTags);
- var innerList = observableDataSource.GetReferenceInnerList();
- var notificationCount = 0;
-
- innerList.CollectionChanged += (s, e) => notificationCount++;
-
- // Act - Nested BeginUpdate/EndUpdate
- innerList.BeginUpdate();
- innerList.BeginUpdate();
- innerList.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 1"));
- innerList.EndUpdate(); // Inner - should not fire
- innerList.Add(new TestDataItem(MockSignalRClient.NextId(), "Item 2"));
- innerList.EndUpdate(); // Outer - should fire once
-
- // Assert
- Assert.AreEqual(1, notificationCount);
- Assert.AreEqual(2, innerList.Count);
- }
-
- #endregion
-
- #region Edge Cases
-
- [TestMethod]
- public void AddRange_AddsMultipleItems()
- {
- // Arrange
- var items = new List
- {
- new(MockSignalRClient.NextId(), "Item 1"),
- new(MockSignalRClient.NextId(), "Item 2"),
- new(MockSignalRClient.NextId(), "Item 3")
- };
-
- // Act
- _dataSource.AddRange(items);
-
- // Assert
- Assert.AreEqual(3, _dataSource.Count);
- }
-
- [TestMethod]
- public void IndexOf_ReturnsCorrectIndex()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- _dataSource.Add(item1);
- _dataSource.Add(item2);
-
- // Act
- var index = _dataSource.IndexOf(item2);
-
- // Assert
- Assert.AreEqual(1, index);
- }
-
- [TestMethod]
- public void IndexOf_NonExisting_ReturnsMinusOne()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Item");
-
- // Act
- var index = _dataSource.IndexOf(item);
-
- // Assert
- Assert.AreEqual(-1, index);
- }
-
- [TestMethod]
- public void TryRemove_RemovesAndReturnsItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource.TryRemove(item.Id, out var removedItem);
-
- // Assert
- Assert.IsTrue(result);
- Assert.IsNotNull(removedItem);
- Assert.AreEqual(item.Id, removedItem.Id);
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- [TestMethod]
- public void TryGetIndex_ExistingId_ReturnsTrueWithIndex()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- // Act
- var result = _dataSource.TryGetIndex(item.Id, out var index);
-
- // Assert
- Assert.IsTrue(result);
- Assert.AreEqual(0, index);
- }
-
- [TestMethod]
- public void CopyTo_CopiesItemsToArray()
- {
- // Arrange
- var item1 = new TestDataItem(MockSignalRClient.NextId(), "Item 1");
- var item2 = new TestDataItem(MockSignalRClient.NextId(), "Item 2");
- _dataSource.Add(item1);
- _dataSource.Add(item2);
-
- var array = new TestDataItem[2];
-
- // Act
- _dataSource.CopyTo(array);
-
- // Assert
- Assert.AreEqual(item1.Id, array[0].Id);
- Assert.AreEqual(item2.Id, array[1].Id);
- }
-
- [TestMethod]
- public void SetWorkingReferenceList_SetsNewInnerList()
- {
- // Arrange
- var newList = new List
- {
- new(MockSignalRClient.NextId(), "Item 1"),
- new(MockSignalRClient.NextId(), "Item 2")
- };
-
- // Act
- _dataSource.SetWorkingReferenceList(newList);
-
- // Assert
- Assert.IsTrue(_dataSource.HasWorkingReferenceList);
- Assert.AreEqual(2, _dataSource.Count);
- Assert.AreSame(newList, _dataSource.GetReferenceInnerList());
- }
-
- [TestMethod]
- public void SetWorkingReferenceList_WithNull_DoesNothing()
- {
- // Act
- _dataSource.SetWorkingReferenceList(null);
-
- // Assert
- Assert.IsFalse(_dataSource.HasWorkingReferenceList);
- }
-
- [TestMethod]
- public void AsReadOnly_ReturnsReadOnlyCollection()
- {
- // Arrange
- _dataSource.Add(new TestDataItem(MockSignalRClient.NextId(), "Item"));
-
- // Act
- var readOnly = _dataSource.AsReadOnly();
-
- // Assert
- Assert.IsNotNull(readOnly);
- Assert.AreEqual(1, readOnly.Count);
- }
-
- #endregion
-
- #region Event Tests
-
- [TestMethod]
- public async Task OnDataSourceLoaded_IsCalledAfterLoad()
- {
- // Arrange
- var loadedEventCalled = false;
- _dataSource.OnDataSourceLoaded = () =>
- {
- loadedEventCalled = true;
- return Task.CompletedTask;
- };
-
- _mockClient.SetupResponse(GetAllTag, new List());
-
- // Act
- await _dataSource.LoadDataSource();
-
- // Assert
- Assert.IsTrue(loadedEventCalled);
- }
-
- [TestMethod]
- public async Task OnDataSourceItemChanged_IsCalledAfterLoadItem()
- {
- // Arrange
- TestDataItem? changedItem = null;
- TrackingState? changedState = null;
-
- _dataSource.OnDataSourceItemChanged = args =>
- {
- changedItem = args.Item;
- changedState = args.TrackingState;
- return Task.CompletedTask;
- };
-
- var id = MockSignalRClient.NextId();
- var item = new TestDataItem(id, "Test Item");
- _mockClient.SetupResponse(GetItemTag, item);
-
- // Act
- await _dataSource.LoadItem(id);
-
- // Assert
- Assert.IsNotNull(changedItem);
- Assert.AreEqual(id, changedItem.Id);
- Assert.AreEqual(TrackingState.Get, changedState);
- }
-
- [TestMethod]
- public async Task IsSyncing_IsTrue_DuringSaveChanges()
- {
- // Arrange
- var wasSyncing = false;
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
-
- _mockClient.SetupResponse(AddTag, req =>
- {
- wasSyncing = _dataSource.IsSyncing;
- return req!;
- });
-
- // Act
- await _dataSource.SaveChanges();
-
- // Assert
- Assert.IsTrue(wasSyncing);
- Assert.IsFalse(_dataSource.IsSyncing); // Should be false after completion
- }
-
- [TestMethod]
- public async Task OnSyncingStateChanged_FiresCorrectly()
- {
- // Arrange
- var syncStates = new List();
- _dataSource.OnSyncingStateChanged += state => syncStates.Add(state);
-
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
- _dataSource.Add(item);
- _mockClient.SetupEchoResponse(AddTag);
-
- // Act
- await _dataSource.SaveChanges();
-
- // Assert
- Assert.AreEqual(2, syncStates.Count);
- Assert.IsTrue(syncStates[0]); // Started
- Assert.IsFalse(syncStates[1]); // Ended
- }
-
- #endregion
-
- #region IList Interface Tests
-
- [TestMethod]
- public void IList_Add_AddsItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
- var list = (System.Collections.IList)_dataSource;
-
- // Act
- var index = list.Add(item);
-
- // Assert
- Assert.AreEqual(0, index);
- Assert.AreEqual(1, _dataSource.Count);
- }
-
- [TestMethod]
- public void IList_Contains_ReturnsCorrectly()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
- _dataSource.Add(item);
- var list = (System.Collections.IList)_dataSource;
-
- // Act & Assert
- Assert.IsTrue(list.Contains(item));
- Assert.IsFalse(list.Contains(new TestDataItem(MockSignalRClient.NextId(), "Other")));
- }
-
- [TestMethod]
- public void IList_IndexOf_ReturnsCorrectIndex()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
- _dataSource.Add(item);
- var list = (System.Collections.IList)_dataSource;
-
- // Act
- var index = list.IndexOf(item);
-
- // Assert
- Assert.AreEqual(0, index);
- }
-
- [TestMethod]
- public void IList_Remove_RemovesItem()
- {
- // Arrange
- var item = new TestDataItem(MockSignalRClient.NextId(), "Test");
- _dataSource.Add(item);
- var list = (System.Collections.IList)_dataSource;
-
- // Act
- list.Remove(item);
-
- // Assert
- Assert.AreEqual(0, _dataSource.Count);
- }
-
- #endregion
-
- #region Context and Filter Tests
-
- [TestMethod]
- public async Task LoadDataSource_WithContextIds_PassesContextToSignalR()
- {
- // Arrange
- var contextIds = new object[] { 123, "SomeFilter" };
- var dataSourceWithContext = new TestDataSource(_mockClient, _crudTags, contextIds);
- _mockClient.SetupResponse(GetAllTag, new List());
-
- // Act
- await dataSourceWithContext.LoadDataSource();
-
- // Assert
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(GetAllTag));
- }
-
- [TestMethod]
- public async Task LoadDataSource_WithFilterText_PassesFilterToSignalR()
- {
- // Arrange
- _dataSource.FilterText = "MyFilter";
- _mockClient.SetupResponse(GetAllTag, new List());
-
- // Act
- await _dataSource.LoadDataSource();
-
- // Assert
- Assert.AreEqual(1, _mockClient.GetSentMessageCount(GetAllTag));
- }
-
- #endregion
-}
diff --git a/AyCode.Services.Server.Tests/SignalRs/ProcessOnReceiveMessageTests.cs b/AyCode.Services.Server.Tests/SignalRs/ProcessOnReceiveMessageTests.cs
deleted file mode 100644
index 2490210..0000000
--- a/AyCode.Services.Server.Tests/SignalRs/ProcessOnReceiveMessageTests.cs
+++ /dev/null
@@ -1,1067 +0,0 @@
-using AyCode.Core.Tests.TestModels;
-using AyCode.Core.Extensions;
-using AyCode.Services.SignalRs;
-using MessagePack;
-
-namespace AyCode.Services.Server.Tests.SignalRs;
-
-///
-/// Base class for ProcessOnReceiveMessage tests.
-/// Tests for AcWebSignalRHubBase.ProcessOnReceiveMessage.
-/// Uses shared DTOs from AyCode.Core.Tests.TestModels.
-/// Derived classes specify the serializer type (JSON or Binary).
-///
-public abstract class ProcessOnReceiveMessageTestsBase
-{
- protected abstract AcSerializerType SerializerType { get; }
-
- protected TestableSignalRHub _hub = null!;
- protected TestSignalRService _service = null!;
-
- [TestInitialize]
- public void Setup()
- {
- _hub = new TestableSignalRHub();
- _service = new TestSignalRService();
- _hub.SetSerializerType(SerializerType);
- _hub.RegisterService(_service);
- }
-
- [TestCleanup]
- public void Cleanup()
- {
- _hub.Reset();
- _service.Reset();
- }
-
- #region Single Primitive Parameter Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_SingleInt_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(42);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, message);
-
- // Assert
- Assert.IsTrue(_service.SingleIntMethodCalled);
- Assert.AreEqual(42, _service.ReceivedInt);
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.SingleIntParam);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_BoolTrue_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(true);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.BoolParam, message);
-
- // Assert
- Assert.IsTrue(_service.BoolMethodCalled);
- Assert.IsTrue(_service.ReceivedBool);
-
- var responseData = SignalRTestHelper.GetResponseData(_hub.SentMessages[0]);
- Assert.IsTrue(responseData, "Response should be true");
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_BoolFalse_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(false);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.BoolParam, message);
-
- // Assert
- Assert.IsTrue(_service.BoolMethodCalled);
- Assert.IsFalse(_service.ReceivedBool);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_String_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage("Hello SignalR!");
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.StringParam, message);
-
- // Assert
- Assert.IsTrue(_service.StringMethodCalled);
- Assert.AreEqual("Hello SignalR!", _service.ReceivedString);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Guid_DeserializesCorrectly()
- {
- // Arrange
- var testGuid = Guid.NewGuid();
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(testGuid);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidParam, message);
-
- // Assert
- Assert.IsTrue(_service.GuidMethodCalled);
- Assert.AreEqual(testGuid, _service.ReceivedGuid);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Enum_DeserializesCorrectly()
- {
- // Arrange - using shared TestStatus enum
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(TestStatus.Active);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.EnumParam, message);
-
- // Assert
- Assert.IsTrue(_service.EnumMethodCalled);
- Assert.AreEqual(TestStatus.Active, _service.ReceivedEnum);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Decimal_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(123.456m);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DecimalParam, message);
-
- // Assert
- Assert.IsTrue(_service.DecimalMethodCalled);
- Assert.AreEqual(123.456m, _service.ReceivedDecimal);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_DateTime_DeserializesCorrectly()
- {
- // Arrange
- var testDate = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc);
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(testDate);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DateTimeParam, message);
-
- // Assert
- Assert.IsTrue(_service.DateTimeMethodCalled);
- Assert.AreEqual(testDate, _service.ReceivedDateTime);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Double_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(3.14159265359);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DoubleParam, message);
-
- // Assert
- Assert.IsTrue(_service.DoubleMethodCalled);
- Assert.IsNotNull(_service.ReceivedDouble);
- Assert.IsTrue(Math.Abs(_service.ReceivedDouble.Value - 3.14159265359) < 0.0000000001);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Long_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(9223372036854775807L);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.LongParam, message);
-
- // Assert
- Assert.IsTrue(_service.LongMethodCalled);
- Assert.AreEqual(9223372036854775807L, _service.ReceivedLong);
- }
-
- #endregion
-
- #region Multiple Primitive Parameters Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_TwoInts_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(10, 20);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TwoIntParams, message);
-
- // Assert
- Assert.IsTrue(_service.TwoIntMethodCalled);
- Assert.AreEqual((10, 20), _service.ReceivedTwoInts);
-
- var responseData = SignalRTestHelper.GetResponseData(_hub.SentMessages[0]);
- Assert.AreEqual(30, responseData, "Sum should be 30");
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_MultipleTypes_DeserializesCorrectly()
- {
- // Arrange
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(true, "test", 123);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.MultipleTypesParams, message);
-
- // Assert
- Assert.IsTrue(_service.MultipleTypesMethodCalled);
- Assert.AreEqual((true, "test", 123), _service.ReceivedMultipleTypes);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_FiveParams_DeserializesCorrectly()
- {
- // Arrange
- var testGuid = Guid.NewGuid();
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(42, "hello", true, testGuid, 99.99m);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.FiveParams, message);
-
- // Assert
- Assert.IsTrue(_service.FiveParamsMethodCalled);
- Assert.IsNotNull(_service.ReceivedFiveParams);
- Assert.AreEqual(42, _service.ReceivedFiveParams.Value.Item1);
- Assert.AreEqual("hello", _service.ReceivedFiveParams.Value.Item2);
- Assert.AreEqual(true, _service.ReceivedFiveParams.Value.Item3);
- Assert.AreEqual(testGuid, _service.ReceivedFiveParams.Value.Item4);
- Assert.AreEqual(99.99m, _service.ReceivedFiveParams.Value.Item5);
- }
-
- #endregion
-
- #region Complex Object Tests (using shared DTOs)
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_TestOrderItem_DeserializesCorrectly()
- {
- // Arrange - using shared TestOrderItem from Core.Tests
- var item = new TestOrderItem
- {
- Id = 1,
- ProductName = "Test Product",
- Quantity = 10,
- UnitPrice = 99.99m,
- Status = TestStatus.Active
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(item);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemParam, message);
-
- // Assert
- Assert.IsTrue(_service.TestOrderItemMethodCalled);
- Assert.IsNotNull(_service.ReceivedTestOrderItem);
- Assert.AreEqual(1, _service.ReceivedTestOrderItem.Id);
- Assert.AreEqual("Test Product", _service.ReceivedTestOrderItem.ProductName);
- Assert.AreEqual(10, _service.ReceivedTestOrderItem.Quantity);
- Assert.AreEqual(99.99m, _service.ReceivedTestOrderItem.UnitPrice);
- Assert.AreEqual(TestStatus.Active, _service.ReceivedTestOrderItem.Status);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_TestOrder_WithNestedItems_DeserializesCorrectly()
- {
- // Arrange - using shared TestOrder with nested items
- var order = new TestOrder
- {
- Id = 100,
- OrderNumber = "ORD-001",
- Status = TestStatus.Active,
- TotalAmount = 500.00m,
- Items =
- [
- new TestOrderItem { Id = 1, ProductName = "Item A", Quantity = 2 },
- new TestOrderItem { Id = 2, ProductName = "Item B", Quantity = 3 }
- ]
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(order);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderParam, message);
-
- // Assert
- Assert.IsTrue(_service.TestOrderMethodCalled);
- Assert.IsNotNull(_service.ReceivedTestOrder);
- Assert.AreEqual(100, _service.ReceivedTestOrder.Id);
- Assert.AreEqual("ORD-001", _service.ReceivedTestOrder.OrderNumber);
- Assert.AreEqual(2, _service.ReceivedTestOrder.Items.Count);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_SharedTag_IIdType_DeserializesCorrectly()
- {
- // Arrange - using shared SharedTag (IId type)
- var tag = new SharedTag
- {
- Id = 1,
- Name = "Important",
- Color = "#FF0000",
- Priority = 1,
- IsActive = true
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(tag);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SharedTagParam, message);
-
- // Assert
- Assert.IsTrue(_service.SharedTagMethodCalled);
- Assert.IsNotNull(_service.ReceivedSharedTag);
- Assert.AreEqual(1, _service.ReceivedSharedTag.Id);
- Assert.AreEqual("Important", _service.ReceivedSharedTag.Name);
- Assert.AreEqual("#FF0000", _service.ReceivedSharedTag.Color);
- }
-
- #endregion
-
- #region No Parameters Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_NoParams_InvokesMethod()
- {
- // Arrange
- var message = SignalRTestHelper.CreateEmptyMessage();
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, message);
-
- // Assert
- Assert.IsTrue(_service.NoParamsMethodCalled);
- SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.NoParams);
- }
-
- #endregion
-
- #region Simple Collection Parameter Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_IntArray_DeserializesCorrectly()
- {
- // Arrange - Arrays are complex objects (not ValueType), use CreateComplexObjectMessage
- var values = new[] { 1, 2, 3, 4, 5 };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.IntArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedIntArray);
- CollectionAssert.AreEqual(values, _service.ReceivedIntArray);
-
- // Verify response (doubled values)
- var responseData = SignalRTestHelper.GetResponseData(_hub.SentMessages[0]);
- Assert.IsNotNull(responseData);
- CollectionAssert.AreEqual(new[] { 2, 4, 6, 8, 10 }, responseData);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_GuidArray_DeserializesCorrectly()
- {
- // Arrange
- var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
- var message = SignalRTestHelper.CreateComplexObjectMessage(guids);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.GuidArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedGuidArray);
- Assert.AreEqual(3, _service.ReceivedGuidArray.Length);
- CollectionAssert.AreEqual(guids, _service.ReceivedGuidArray);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_StringList_DeserializesCorrectly()
- {
- // Arrange
- var items = new List { "apple", "banana", "cherry" };
- var message = SignalRTestHelper.CreateComplexObjectMessage(items);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.StringListParam, message);
-
- // Assert
- Assert.IsTrue(_service.StringListMethodCalled);
- Assert.IsNotNull(_service.ReceivedStringList);
- CollectionAssert.AreEqual(items, _service.ReceivedStringList);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_IntList_DeserializesCorrectly()
- {
- // Arrange
- var numbers = new List { 10, 20, 30 };
- var message = SignalRTestHelper.CreateComplexObjectMessage(numbers);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntListParam, message);
-
- // Assert
- Assert.IsTrue(_service.IntListMethodCalled);
- Assert.IsNotNull(_service.ReceivedIntList);
- CollectionAssert.AreEqual(numbers, _service.ReceivedIntList);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_BoolArray_DeserializesCorrectly()
- {
- // Arrange
- var flags = new[] { true, false, true, true };
- var message = SignalRTestHelper.CreateComplexObjectMessage(flags);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.BoolArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.BoolArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedBoolArray);
- CollectionAssert.AreEqual(flags, _service.ReceivedBoolArray);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyIntArray_DeserializesCorrectly()
- {
- // Arrange
- var values = Array.Empty();
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.IntArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedIntArray);
- Assert.AreEqual(0, _service.ReceivedIntArray.Length);
- }
-
- #endregion
-
- #region Complex Collection Parameter Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_TestOrderItemList_DeserializesCorrectly()
- {
- // Arrange - using shared TestOrderItem list
- var items = new List
- {
- new() { Id = 1, ProductName = "First", Quantity = 1 },
- new() { Id = 2, ProductName = "Second", Quantity = 2 },
- new() { Id = 3, ProductName = "Third", Quantity = 3 }
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(items);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemListParam, message);
-
- // Assert
- Assert.IsTrue(_service.TestOrderItemListMethodCalled);
- Assert.AreEqual(3, _service.ReceivedTestOrderItemList?.Count);
- Assert.AreEqual("First", _service.ReceivedTestOrderItemList?[0].ProductName);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_NestedList_DeserializesCorrectly()
- {
- // Arrange
- var nestedList = new List>
- {
- new() { 1, 2, 3 },
- new() { 4, 5 },
- new() { 6, 7, 8, 9 }
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(nestedList);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NestedListParam, message);
-
- // Assert
- Assert.IsTrue(_service.NestedListMethodCalled);
- Assert.IsNotNull(_service.ReceivedNestedList);
- Assert.AreEqual(3, _service.ReceivedNestedList.Count);
- CollectionAssert.AreEqual(new List { 1, 2, 3 }, _service.ReceivedNestedList[0]);
- }
-
- #endregion
-
- #region Mixed Parameter Tests (Primitive + Complex)
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_IntAndDto_DeserializesCorrectly()
- {
- // Arrange
- var item = new TestOrderItem { Id = 10, ProductName = "Test", Quantity = 5 };
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(42, item);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntAndDtoParam, message);
-
- // Assert
- Assert.IsTrue(_service.IntAndDtoMethodCalled);
- Assert.IsNotNull(_service.ReceivedIntAndDto);
- Assert.AreEqual(42, _service.ReceivedIntAndDto.Value.Item1);
- Assert.AreEqual("Test", _service.ReceivedIntAndDto.Value.Item2?.ProductName);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_DtoAndList_DeserializesCorrectly()
- {
- // Arrange
- var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 1 };
- var numbers = new List { 1, 2, 3 };
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(item, numbers);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DtoAndListParam, message);
-
- // Assert
- Assert.IsTrue(_service.DtoAndListMethodCalled);
- Assert.IsNotNull(_service.ReceivedDtoAndList);
- Assert.AreEqual("Test", _service.ReceivedDtoAndList.Value.Item1?.ProductName);
- CollectionAssert.AreEqual(numbers, _service.ReceivedDtoAndList.Value.Item2);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_ThreeComplexParams_DeserializesCorrectly()
- {
- // Arrange
- var item = new TestOrderItem { Id = 1, ProductName = "Product", Quantity = 1 };
- var tags = new List { "tag1", "tag2" };
- var sharedTag = new SharedTag { Id = 1, Name = "Shared" };
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(item, tags, sharedTag);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.ThreeComplexParams, message);
-
- // Assert
- Assert.IsTrue(_service.ThreeComplexParamsMethodCalled);
- Assert.IsNotNull(_service.ReceivedThreeComplexParams);
- Assert.AreEqual("Product", _service.ReceivedThreeComplexParams.Value.Item1?.ProductName);
- Assert.AreEqual(2, _service.ReceivedThreeComplexParams.Value.Item2?.Count);
- Assert.AreEqual("Shared", _service.ReceivedThreeComplexParams.Value.Item3?.Name);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_MixedWithArray_DeserializesCorrectly()
- {
- // Arrange - Multiple params: bool, int[], string
- // When ParamCount > 1, uses IdMessage format regardless of types
- var flag = true;
- var numbers = new[] { 1, 2, 3 };
- var text = "hello";
- var message = SignalRTestHelper.CreatePrimitiveParamsMessage(flag, numbers, text);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.MixedWithArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.MixedWithArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedMixedWithArray);
- Assert.AreEqual(true, _service.ReceivedMixedWithArray.Value.Item1);
- CollectionAssert.AreEqual(numbers, _service.ReceivedMixedWithArray.Value.Item2);
- Assert.AreEqual("hello", _service.ReceivedMixedWithArray.Value.Item3);
- }
-
- #endregion
-
- #region Error Handling Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_UnknownTag_InvokesNotFoundCallback()
- {
- // Arrange
- const int unknownTag = 9999;
- var message = SignalRTestHelper.CreateEmptyMessage();
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(unknownTag, message);
-
- // Assert
- Assert.IsTrue(_hub.WasNotFoundCallbackInvoked);
- Assert.IsNotNull(_hub.NotFoundTagName);
-
- // Should still send error response
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], unknownTag);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_MethodThrows_ReturnsErrorResponse()
- {
- // Arrange
- var message = SignalRTestHelper.CreateEmptyMessage();
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.ThrowsException, message);
-
- // Assert
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.ThrowsException);
- Assert.IsTrue(_hub.Logger.HasErrorLogs, "Should have logged error");
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyMessage_LogsWarning()
- {
- // Arrange
- byte[] emptyMessage = [];
-
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, emptyMessage);
-
- // Assert
- Assert.IsTrue(_hub.Logger.HasWarningLogs, "Should have warning for empty message");
- }
-
- #endregion
-
- #region Response Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Success_SendsToCaller()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(42);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, message);
-
- // Assert
- Assert.AreEqual(1, _hub.SentMessages.Count);
- Assert.AreEqual(SendTarget.Caller, _hub.SentMessages[0].Target);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_WithRequestId_IncludesRequestIdInResponse()
- {
- // Arrange
- var message = SignalRTestHelper.CreateSinglePrimitiveMessage(42);
- const int requestId = 12345;
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, message, requestId);
-
- // Assert
- Assert.AreEqual(requestId, _hub.SentMessages[0].RequestId);
- }
-
- #endregion
-
- #region Empty/Null Message Edge Cases
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyBytes_WithNoParams_Succeeds()
- {
- // Arrange - empty byte array for method with no parameters
- byte[] emptyMessage = [];
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, emptyMessage);
-
- // Assert - should succeed because method has no params
- Assert.IsTrue(_service.NoParamsMethodCalled);
- Assert.IsTrue(_hub.Logger.HasWarningLogs, "Should have warning for empty message");
- SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.NoParams);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyBytes_WithIntParam_ReturnsError()
- {
- // Arrange - empty byte array for method expecting int parameter
- byte[] emptyMessage = [];
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, emptyMessage);
-
- // Assert - should return error response, not crash
- Assert.IsFalse(_service.SingleIntMethodCalled, "Method should not be called with empty message");
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.SingleIntParam);
- Assert.IsTrue(_hub.Logger.HasErrorLogs || _hub.Logger.HasWarningLogs);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyBytes_WithComplexParam_ReturnsError()
- {
- // Arrange - empty byte array for method expecting TestOrderItem parameter
- byte[] emptyMessage = [];
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemParam, emptyMessage);
-
- // Assert - should return error response, not crash
- Assert.IsFalse(_service.TestOrderItemMethodCalled, "Method should not be called with empty message");
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.TestOrderItemParam);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_NullMessage_WithNoParams_Succeeds()
- {
- // Arrange - null message for method with no parameters
- byte[]? nullMessage = null;
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.NoParams, nullMessage);
-
- // Assert - should succeed because method has no params
- Assert.IsTrue(_service.NoParamsMethodCalled);
- SignalRTestHelper.AssertSuccessResponse(_hub.SentMessages[0], TestSignalRTags.NoParams);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_NullMessage_WithIntParam_ReturnsError()
- {
- // Arrange - null message for method expecting int parameter
- byte[]? nullMessage = null;
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SingleIntParam, nullMessage);
-
- // Assert - should return error response, not crash
- Assert.IsFalse(_service.SingleIntMethodCalled, "Method should not be called with null message");
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.SingleIntParam);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyBytes_WithMultipleParams_ReturnsError()
- {
- // Arrange - empty byte array for method expecting multiple parameters
- byte[] emptyMessage = [];
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TwoIntParams, emptyMessage);
-
- // Assert - should return error response, not crash
- Assert.IsFalse(_service.TwoIntMethodCalled, "Method should not be called with empty message");
- Assert.AreEqual(1, _hub.SentMessages.Count);
- SignalRTestHelper.AssertErrorResponse(_hub.SentMessages[0], TestSignalRTags.TwoIntParams);
- }
-
- #endregion
-
- #region Extended Array Parameter Deserialization Tests
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_LongArray_DeserializesCorrectly()
- {
- // Arrange
- var values = new[] { 1L, 9223372036854775807L, -9223372036854775808L, 0L };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.LongArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.LongArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedLongArray);
- Assert.AreEqual(4, _service.ReceivedLongArray.Length);
- Assert.AreEqual(9223372036854775807L, _service.ReceivedLongArray[1]);
- Assert.AreEqual(-9223372036854775808L, _service.ReceivedLongArray[2]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_DecimalArray_DeserializesCorrectly()
- {
- // Arrange
- var values = new[] { 0.01m, 99.99m, 123456.789m, -999.99m };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DecimalArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.DecimalArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedDecimalArray);
- Assert.AreEqual(4, _service.ReceivedDecimalArray.Length);
- Assert.AreEqual(0.01m, _service.ReceivedDecimalArray[0]);
- Assert.AreEqual(99.99m, _service.ReceivedDecimalArray[1]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_DateTimeArray_DeserializesCorrectly()
- {
- // Arrange
- var values = new[]
- {
- new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
- new DateTime(2024, 12, 31, 23, 59, 59, DateTimeKind.Utc),
- DateTime.MinValue,
- new DateTime(2000, 6, 15, 12, 30, 45, DateTimeKind.Utc)
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DateTimeArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.DateTimeArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedDateTimeArray);
- Assert.AreEqual(4, _service.ReceivedDateTimeArray.Length);
- Assert.AreEqual(2024, _service.ReceivedDateTimeArray[0].Year);
- Assert.AreEqual(12, _service.ReceivedDateTimeArray[1].Month);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EnumArray_DeserializesCorrectly()
- {
- // Arrange - using shared TestStatus enum
- var values = new[] { TestStatus.Pending, TestStatus.Active, TestStatus.Processing, TestStatus.Shipped };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.EnumArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.EnumArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedEnumArray);
- Assert.AreEqual(4, _service.ReceivedEnumArray.Length);
- Assert.AreEqual(TestStatus.Pending, _service.ReceivedEnumArray[0]);
- Assert.AreEqual(TestStatus.Shipped, _service.ReceivedEnumArray[3]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_DoubleArray_DeserializesCorrectly()
- {
- // Arrange
- var values = new[] { 3.14159265359, -273.15, 0.0, double.MaxValue, double.MinValue };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DoubleArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.DoubleArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedDoubleArray);
- Assert.AreEqual(5, _service.ReceivedDoubleArray.Length);
- Assert.IsTrue(Math.Abs(_service.ReceivedDoubleArray[0] - 3.14159265359) < 0.0000001);
- Assert.AreEqual(-273.15, _service.ReceivedDoubleArray[1]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_SharedTagArray_ComplexObjectArray_DeserializesCorrectly()
- {
- // Arrange - array of IId complex objects
- var tags = new[]
- {
- new SharedTag { Id = 1, Name = "Tag1", Color = "#FF0000", Priority = 1 },
- new SharedTag { Id = 2, Name = "Tag2", Color = "#00FF00", Priority = 2 },
- new SharedTag { Id = 3, Name = "Tag3", Color = "#0000FF", Priority = 3 }
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(tags);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.SharedTagArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.SharedTagArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedSharedTagArray);
- Assert.AreEqual(3, _service.ReceivedSharedTagArray.Length);
- Assert.AreEqual("Tag1", _service.ReceivedSharedTagArray[0].Name);
- Assert.AreEqual("#00FF00", _service.ReceivedSharedTagArray[1].Color);
- Assert.AreEqual(3, _service.ReceivedSharedTagArray[2].Priority);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_Dictionary_DeserializesCorrectly()
- {
- // Arrange
- var dict = new Dictionary
- {
- { "apple", 1 },
- { "banana", 2 },
- { "cherry", 3 }
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(dict);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.DictionaryParam, message);
-
- // Assert
- Assert.IsTrue(_service.DictionaryMethodCalled);
- Assert.IsNotNull(_service.ReceivedDictionary);
- Assert.AreEqual(3, _service.ReceivedDictionary.Count);
- Assert.AreEqual(1, _service.ReceivedDictionary["apple"]);
- Assert.AreEqual(2, _service.ReceivedDictionary["banana"]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_EmptyGuidArray_DeserializesCorrectly()
- {
- // Arrange - edge case: empty Guid array
- var guids = Array.Empty();
- var message = SignalRTestHelper.CreateComplexObjectMessage(guids);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.GuidArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedGuidArray);
- Assert.AreEqual(0, _service.ReceivedGuidArray.Length);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_SingleElementArray_DeserializesCorrectly()
- {
- // Arrange - edge case: single element array
- var values = new[] { 42 };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.IntArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedIntArray);
- Assert.AreEqual(1, _service.ReceivedIntArray.Length);
- Assert.AreEqual(42, _service.ReceivedIntArray[0]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_LargeArray_DeserializesCorrectly()
- {
- // Arrange - performance edge case: large array
- var values = Enumerable.Range(1, 1000).ToArray();
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.IntArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.IntArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedIntArray);
- Assert.AreEqual(1000, _service.ReceivedIntArray.Length);
- Assert.AreEqual(1, _service.ReceivedIntArray[0]);
- Assert.AreEqual(1000, _service.ReceivedIntArray[999]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_ComplexObjectListWithNestedCollections_DeserializesCorrectly()
- {
- // Arrange - TestOrder contains nested Items list
- var orders = new List
- {
- new() { Id = 1, ProductName = "Product A", Quantity = 10, UnitPrice = 9.99m, Status = TestStatus.Active },
- new() { Id = 2, ProductName = "Product B", Quantity = 5, UnitPrice = 19.99m, Status = TestStatus.Processing },
- new() { Id = 3, ProductName = "Product C", Quantity = 1, UnitPrice = 99.99m, Status = TestStatus.Shipped }
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(orders);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.TestOrderItemListParam, message);
-
- // Assert
- Assert.IsTrue(_service.TestOrderItemListMethodCalled);
- Assert.IsNotNull(_service.ReceivedTestOrderItemList);
- Assert.AreEqual(3, _service.ReceivedTestOrderItemList.Count);
- Assert.AreEqual("Product A", _service.ReceivedTestOrderItemList[0].ProductName);
- Assert.AreEqual(19.99m, _service.ReceivedTestOrderItemList[1].UnitPrice);
- Assert.AreEqual(TestStatus.Shipped, _service.ReceivedTestOrderItemList[2].Status);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_StringArrayWithSpecialChars_DeserializesCorrectly()
- {
- // Arrange - strings with special characters
- var values = new List
- {
- "normal",
- "with spaces",
- "with\"quotes\"",
- "with\nnewline",
- "with\ttab",
- "unicode: áéíóú",
- "" // empty string
- };
- var message = SignalRTestHelper.CreateComplexObjectMessage(values);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.StringListParam, message);
-
- // Assert
- Assert.IsTrue(_service.StringListMethodCalled);
- Assert.IsNotNull(_service.ReceivedStringList);
- Assert.AreEqual(7, _service.ReceivedStringList.Count);
- Assert.AreEqual("normal", _service.ReceivedStringList[0]);
- Assert.AreEqual("with spaces", _service.ReceivedStringList[1]);
- Assert.AreEqual("unicode: áéíóú", _service.ReceivedStringList[5]);
- Assert.AreEqual("", _service.ReceivedStringList[6]);
- }
-
- [TestMethod]
- public async Task ProcessOnReceiveMessage_GuidArrayWithEmptyGuid_DeserializesCorrectly()
- {
- // Arrange - includes Guid.Empty
- var guids = new[] { Guid.NewGuid(), Guid.Empty, Guid.NewGuid() };
- var message = SignalRTestHelper.CreateComplexObjectMessage(guids);
-
- // Act
- await _hub.InvokeProcessOnReceiveMessage(TestSignalRTags.GuidArrayParam, message);
-
- // Assert
- Assert.IsTrue(_service.GuidArrayMethodCalled);
- Assert.IsNotNull(_service.ReceivedGuidArray);
- Assert.AreEqual(3, _service.ReceivedGuidArray.Length);
- Assert.AreEqual(Guid.Empty, _service.ReceivedGuidArray[1]);
- }
-
- [TestMethod]
- public void CreateComplexObjectMessage_Dictionary_ProducesCorrectJson()
- {
- // Arrange
- var dict = new Dictionary
- {
- { "apple", 1 },
- { "banana", 2 },
- { "cherry", 3 }
- };
-
- // Act
- var message = SignalRTestHelper.CreateComplexObjectMessage(dict);
- var deserialized = MessagePackSerializer.Deserialize>(
- message, MessagePackSerializerOptions.Standard);
-
- // Assert
- Assert.IsNotNull(deserialized.PostDataJson, "PostDataJson should not be null");
- Assert.IsTrue(deserialized.PostDataJson.Contains("apple"),
- $"PostDataJson should contain 'apple'. Actual: {deserialized.PostDataJson}");
- Assert.IsTrue(deserialized.PostDataJson.StartsWith("{"),
- $"PostDataJson should start with {{. Actual first char: {deserialized.PostDataJson[0]}");
- }
-
- #endregion
-}
-
-///
-/// Runs all ProcessOnReceiveMessage tests with JSON serialization.
-///
-[TestClass]
-public class ProcessOnReceiveMessageTests_Json : ProcessOnReceiveMessageTestsBase
-{
- protected override AcSerializerType SerializerType => AcSerializerType.Json;
-}
-
-///
-/// Runs all ProcessOnReceiveMessage tests with Binary serialization.
-///
-[TestClass]
-public class ProcessOnReceiveMessageTests_Binary : ProcessOnReceiveMessageTestsBase
-{
- protected override AcSerializerType SerializerType => AcSerializerType.Binary;
-}
diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs
new file mode 100644
index 0000000..a1ea8f6
--- /dev/null
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDataSourceTests.cs
@@ -0,0 +1,172 @@
+using AyCode.Core.Enums;
+using AyCode.Core.Extensions;
+using AyCode.Core.Helpers;
+using AyCode.Core.Tests.TestModels;
+using AyCode.Services.Server.SignalRs;
+using AyCode.Services.SignalRs;
+
+namespace AyCode.Services.Server.Tests.SignalRs;
+
+#region DataSource Implementations
+
+public class TestOrderItemListDataSource : AcSignalRDataSource>
+{
+ public TestOrderItemListDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
+ : base(signalRClient, crudTags) { }
+}
+
+public class TestOrderItemObservableDataSource : AcSignalRDataSource>
+{
+ public TestOrderItemObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags)
+ : base(signalRClient, crudTags) { }
+}
+
+#endregion
+
+#region Abstract Test Base
+
+///
+/// Base class for SignalR DataSource tests.
+/// Derived classes specify the serializer type and collection type.
+///
+/// The concrete DataSource type
+/// The inner list type (List or AcObservableCollection)
+public abstract class SignalRDataSourceTestBase
+ where TDataSource : AcSignalRDataSource
+ where TIList : class, IList
+{
+ protected abstract AcSerializerOptions SerializerOption { get; }
+ protected abstract TDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags);
+
+ protected TestLogger _logger = null!;
+ protected TestableSignalRHub2 _hub = null!;
+ protected TestableSignalRClient2 _client = null!;
+ protected TestSignalRService2 _service = null!;
+ protected SignalRCrudTags _crudTags = 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);
+
+ _crudTags = new SignalRCrudTags(
+ TestSignalRTags.DataSourceGetAll,
+ TestSignalRTags.DataSourceGetItem,
+ TestSignalRTags.DataSourceAdd,
+ TestSignalRTags.DataSourceUpdate,
+ TestSignalRTags.DataSourceRemove
+ );
+ }
+
+ #region Load Tests
+
+ [TestMethod]
+ public async Task LoadDataSource_ReturnsAllItems()
+ {
+ var dataSource = CreateDataSource(_client, _crudTags);
+
+ await dataSource.LoadDataSource();
+
+ Assert.AreEqual(3, dataSource.Count);
+ Assert.AreEqual("Product A", dataSource[0].ProductName);
+ }
+
+ [TestMethod]
+ public async Task LoadItem_ReturnsSingleItem()
+ {
+ var dataSource = CreateDataSource(_client, _crudTags);
+
+ var result = await dataSource.LoadItem(2);
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(2, result.Id);
+ Assert.AreEqual("Product B", result.ProductName);
+ }
+
+ #endregion
+
+ #region Add Tests
+
+ [TestMethod]
+ public async Task Add_WithAutoSave_AddsItem()
+ {
+ var dataSource = CreateDataSource(_client, _crudTags);
+ var newItem = new TestOrderItem { Id = 100, ProductName = "New Product", Quantity = 5, UnitPrice = 50m };
+
+ var result = await dataSource.Add(newItem, autoSave: true);
+
+ Assert.AreEqual(1, dataSource.Count);
+ Assert.AreEqual("New Product", result.ProductName);
+ }
+
+ [TestMethod]
+ public void Add_WithoutAutoSave_AddsToTrackingOnly()
+ {
+ var dataSource = CreateDataSource(_client, _crudTags);
+ var newItem = new TestOrderItem { Id = 100, ProductName = "New Product" };
+
+ dataSource.Add(newItem);
+
+ Assert.AreEqual(1, dataSource.Count);
+ Assert.AreEqual(1, dataSource.GetTrackingItems().Count);
+ Assert.AreEqual(TrackingState.Add, dataSource.GetTrackingItems()[0].TrackingState);
+ }
+
+ #endregion
+
+ #region SaveChanges Tests
+
+ [TestMethod]
+ public async Task SaveChanges_SavesTrackedItems()
+ {
+ var dataSource = CreateDataSource(_client, _crudTags);
+ dataSource.Add(new TestOrderItem { Id = 101, ProductName = "Item 1" });
+ dataSource.Add(new TestOrderItem { Id = 102, ProductName = "Item 2" });
+
+ var unsaved = await dataSource.SaveChanges();
+
+ Assert.AreEqual(0, unsaved.Count);
+ Assert.AreEqual(0, dataSource.GetTrackingItems().Count);
+ }
+
+ [TestMethod]
+ public async Task SaveChangesAsync_ClearsTracking()
+ {
+ var dataSource = CreateDataSource(_client, _crudTags);
+ dataSource.Add(new TestOrderItem { Id = 103, ProductName = "Item 3" });
+
+ await dataSource.SaveChangesAsync();
+
+ Assert.AreEqual(0, dataSource.GetTrackingItems().Count);
+ }
+
+ #endregion
+}
+
+#endregion
+
+#region DataSources
+
+[TestClass]
+public class SignalRDataSourceTests_List : SignalRDataSourceTestBase>
+{
+ protected override AcSerializerOptions SerializerOption => AcBinarySerializerOptions.Default;
+ protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
+ => new(client, crudTags);
+}
+
+[TestClass]
+public class SignalRDataSourceTests_Observable : SignalRDataSourceTestBase>
+{
+ protected override AcSerializerOptions SerializerOption => AcBinarySerializerOptions.Default;
+
+ protected override TestOrderItemObservableDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
+ => new(client, crudTags);
+}
+#endregion
diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRTestHelper.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRTestHelper.cs
index 8b8897a..733af7c 100644
--- a/AyCode.Services.Server.Tests/SignalRs/SignalRTestHelper.cs
+++ b/AyCode.Services.Server.Tests/SignalRs/SignalRTestHelper.cs
@@ -5,18 +5,39 @@ using MessagePack.Resolvers;
namespace AyCode.Services.Server.Tests.SignalRs;
+///
+/// Captured sent message for assertions.
+///
+public record SentMessage(
+ int MessageTag,
+ ISignalRMessage Message,
+ int? RequestId,
+ SendTarget Target,
+ string? TargetId = null)
+{
+ public SignalResponseDataMessage? AsDataResponse => Message as SignalResponseDataMessage;
+}
+
+///
+/// Target of the sent message.
+///
+public enum SendTarget
+{
+ Caller,
+ Client,
+ Others,
+ All,
+ User,
+ Group
+}
+
///
/// Helper methods for creating SignalR test messages.
-/// Uses the production SignalR types for compatibility with the actual client/server code.
///
public static class SignalRTestHelper
{
private static readonly MessagePackSerializerOptions MessagePackOptions = ContractlessStandardResolver.Options;
- ///
- /// Creates a MessagePack message for parameters using IdMessage format.
- /// Each parameter is serialized directly as JSON (no array wrapping).
- ///
public static byte[] CreatePrimitiveParamsMessage(params object[] values)
{
var idMessage = new IdMessage(values);
@@ -24,18 +45,9 @@ public static class SignalRTestHelper
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
}
- ///
- /// Creates a MessagePack message for a single primitive parameter.
- ///
public static byte[] CreateSinglePrimitiveMessage(T value) where T : notnull
- {
- return CreatePrimitiveParamsMessage(value);
- }
+ => CreatePrimitiveParamsMessage(value);
- ///
- /// Creates a MessagePack message for a complex object parameter.
- /// Uses PostDataJson pattern for single complex objects.
- ///
public static byte[] CreateComplexObjectMessage(T obj)
{
var json = obj.ToJson();
@@ -43,73 +55,39 @@ public static class SignalRTestHelper
return MessagePackSerializer.Serialize(postMessage, MessagePackOptions);
}
- ///
- /// Creates an empty MessagePack message for parameterless methods.
- ///
public static byte[] CreateEmptyMessage()
{
var postMessage = new SignalPostJsonDataMessage