1302 lines
37 KiB
C#
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
|
|
}
|