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

1302 lines
37 KiB
C#

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<int>
{
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<TestDataItem, int, List<TestDataItem>>
{
public TestDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
: base(signalRClient, signalRCrudTags, contextIds)
{
}
}
public class TestObservableDataSource : AcSignalRDataSource<TestDataItem, int, AcObservableCollection<TestDataItem>>
{
public TestObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
: base(signalRClient, signalRCrudTags, contextIds)
{
}
}
#endregion
#region Mock SignalR Client
/// <summary>
/// Mock SignalR client for testing AcSignalRDataSource without actual network calls.
/// Uses the test constructor to avoid real HubConnection.
/// </summary>
public class MockSignalRClient : AcSignalRClientBase
{
private readonly ConcurrentDictionary<int, Func<object?, object?>> _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;
/// <summary>
/// Uses test constructor - no real HubConnection created.
/// </summary>
public MockSignalRClient() : base(new MockLogger())
{
}
/// <summary>
/// Setup a static response for a specific message tag
/// </summary>
public void SetupResponse<TResponse>(int messageTag, TResponse response)
{
_responseHandlers[messageTag] = _ => response;
}
/// <summary>
/// Setup a dynamic response based on the request
/// </summary>
public void SetupResponse<TRequest, TResponse>(int messageTag, Func<TRequest?, TResponse> responseFactory)
{
_responseHandlers[messageTag] = req => responseFactory((TRequest?)req);
}
/// <summary>
/// Setup a response that returns the posted data (echo)
/// </summary>
public void SetupEchoResponse<T>(int messageTag) where T : class
{
_responseHandlers[messageTag] = req => req;
}
/// <summary>
/// Clear all response handlers
/// </summary>
public void ClearResponses()
{
_responseHandlers.Clear();
}
public override Task<TResponseData?> GetAllAsync<TResponseData>(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<TResponseData>(int messageTag, Func<ISignalResponseMessage<TResponseData?>, 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<TResponseData?> message = new SignalResponseMessage<TResponseData?>(messageTag, SignalResponseStatus.Success, responseJson);
return responseCallback(message);
}
ISignalResponseMessage<TResponseData?> errorMessage = new SignalResponseMessage<TResponseData?>(messageTag, SignalResponseStatus.Error, (string?)null);
return responseCallback(errorMessage);
}
public override Task<TResponseData?> GetByIdAsync<TResponseData>(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<TPostData?> PostDataAsync<TPostData>(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<TPostData?>(postData);
}
public override Task PostDataAsync<TPostData>(int messageTag, TPostData postData, Func<ISignalResponseMessage<TPostData?>, 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<TPostData?> message = new SignalResponseMessage<TPostData?>(messageTag, SignalResponseStatus.Success, responseJson);
return responseCallback(message);
}
// Default: echo back the posted data
ISignalResponseMessage<TPostData?> successMessage = new SignalResponseMessage<TPostData?>(messageTag, SignalResponseStatus.Success, postData?.ToJson());
return responseCallback(successMessage);
}
protected override Task MessageReceived(int messageTag, byte[] messageBytes)
{
return Task.CompletedTask;
}
}
/// <summary>
/// Silent logger for testing - does not require appsettings.json
/// </summary>
public class MockLogger : AcLoggerBase
{
private readonly List<string> _logs = new();
public IReadOnlyList<string> 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<TestDataItem>(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<TestDataItem>(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<TestDataItem>(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<TestDataItem>(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<TestDataItem>(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<TestDataItem>(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<TestDataItem>(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<TestDataItem>
{
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<TestDataItem?>(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<TestDataItem>
{
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<Task>();
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<Task>();
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<TestDataItem>(AddTag);
var tasks = new List<Task<TestDataItem>>();
// 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<TestDataItem>();
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<TestDataItem>
{
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<TestDataItem>
{
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<TestDataItem>());
// 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<TestDataItem, TestDataItem>(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<bool>();
_dataSource.OnSyncingStateChanged += state => syncStates.Add(state);
var item = new TestDataItem(MockSignalRClient.NextId(), "Test Item");
_dataSource.Add(item);
_mockClient.SetupEchoResponse<TestDataItem>(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<TestDataItem>());
// 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<TestDataItem>());
// Act
await _dataSource.LoadDataSource();
// Assert
Assert.AreEqual(1, _mockClient.GetSentMessageCount(GetAllTag));
}
#endregion
}