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 }