AyCode.Core/AyCode.Services.Tests/SignalRs/TestableSignalRClient.cs

232 lines
7.3 KiB
C#

using AyCode.Core;
using AyCode.Core.Extensions;
using AyCode.Core.Tests.TestModels;
using AyCode.Services.SignalRs;
using MessagePack.Resolvers;
using Microsoft.AspNetCore.SignalR.Client;
namespace AyCode.Services.Tests.SignalRs;
/// <summary>
/// Testable SignalR client that allows testing without real HubConnection.
/// </summary>
public class TestableSignalRClient : AcSignalRClientBase
{
private HubConnectionState _connectionState = HubConnectionState.Connected;
private int? _nextRequestIdOverride;
/// <summary>
/// Messages sent to the server (captured for assertions).
/// </summary>
public List<SentClientMessage> SentMessages { get; } = [];
/// <summary>
/// Received messages (captured for assertions).
/// </summary>
public List<ReceivedClientMessage> ReceivedMessages { get; } = [];
public TestableSignalRClient(TestLogger logger) : base(logger)
{
}
#region Override virtual methods for testing
protected override HubConnectionState GetConnectionState() => _connectionState;
protected override bool IsConnected() => _connectionState == HubConnectionState.Connected;
protected override Task StartConnectionInternal()
{
_connectionState = HubConnectionState.Connected;
return Task.CompletedTask;
}
protected override Task StopConnectionInternal()
{
_connectionState = HubConnectionState.Disconnected;
return Task.CompletedTask;
}
protected override ValueTask DisposeConnectionInternal() => ValueTask.CompletedTask;
protected override Task SendToHubAsync(int messageTag, byte[]? messageBytes, int? requestId)
{
SentMessages.Add(new SentClientMessage(messageTag, messageBytes, requestId));
return Task.CompletedTask;
}
protected override int GetNextRequestId()
{
if (_nextRequestIdOverride.HasValue)
{
var id = _nextRequestIdOverride.Value;
_nextRequestIdOverride = id + 1; // Auto-increment for subsequent calls
return id;
}
return AcDomain.NextUniqueInt32;
}
protected override Task MessageReceived(int messageTag, byte[] messageBytes)
{
ReceivedMessages.Add(new ReceivedClientMessage(messageTag, messageBytes));
return Task.CompletedTask;
}
#endregion
#region Public test helpers (wrappers for protected methods)
/// <summary>
/// Sets the simulated connection state.
/// </summary>
public void SetConnectionState(HubConnectionState state) => _connectionState = state;
/// <summary>
/// Sets the next request ID for deterministic testing.
/// Will auto-increment for subsequent calls.
/// </summary>
public void SetNextRequestId(int id) => _nextRequestIdOverride = id;
/// <summary>
/// Gets the pending requests dictionary (public wrapper for testing).
/// </summary>
public new System.Collections.Concurrent.ConcurrentDictionary<int, SignalRRequestModel> GetPendingRequests()
=> base.GetPendingRequests();
/// <summary>
/// Registers a pending request (public wrapper for testing).
/// </summary>
public new void RegisterPendingRequest(int requestId, SignalRRequestModel model)
=> base.RegisterPendingRequest(requestId, model);
/// <summary>
/// Clears pending requests (public wrapper for testing).
/// </summary>
public new void ClearPendingRequests() => base.ClearPendingRequests();
/// <summary>
/// Simulates receiving a response from the server.
/// </summary>
public Task SimulateServerResponse(int requestId, int messageTag, SignalResponseStatus status, object? data = null)
{
var response = new SignalResponseDataMessage(messageTag, status, data, AcJsonSerializerOptions.Default);
var bytes = response.ToBinary();
return OnReceiveMessage(messageTag, bytes, requestId);
}
/// <summary>
/// Simulates receiving a success response from the server.
/// </summary>
public Task SimulateSuccessResponse<T>(int requestId, int messageTag, T data)
=> SimulateServerResponse(requestId, messageTag, SignalResponseStatus.Success, data);
/// <summary>
/// Simulates receiving an error response from the server.
/// </summary>
public Task SimulateErrorResponse(int requestId, int messageTag)
=> SimulateServerResponse(requestId, messageTag, SignalResponseStatus.Error);
/// <summary>
/// Gets the last sent message.
/// </summary>
public SentClientMessage? LastSentMessage => SentMessages.LastOrDefault();
/// <summary>
/// Clears all captured messages.
/// </summary>
public void ClearMessages()
{
SentMessages.Clear();
ReceivedMessages.Clear();
}
/// <summary>
/// Invokes OnReceiveMessage directly for testing.
/// </summary>
public Task InvokeOnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId)
=> OnReceiveMessage(messageTag, messageBytes, requestId);
#endregion
}
/// <summary>
/// Represents a message sent from client to server.
/// </summary>
public record SentClientMessage(int MessageTag, byte[]? MessageBytes, int? RequestId)
{
/// <summary>
/// Deserializes the message to IdMessage format.
/// Works with both production SignalPostJsonDataMessage and test SignalRPostMessageDto.
/// </summary>
public IdMessage? AsIdMessage()
{
if (MessageBytes == null) return null;
try
{
// First deserialize to get the PostDataJson string
var msg = MessageBytes.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>(ContractlessStandardResolver.Options);
return msg.PostData;
}
catch
{
// Fallback: try deserializing as raw JSON wrapper
try
{
var rawMsg = MessageBytes.MessagePackTo<SignalPostJsonMessage>(ContractlessStandardResolver.Options);
return rawMsg.PostDataJson?.JsonTo<IdMessage>();
}
catch
{
return null;
}
}
}
/// <summary>
/// Deserializes the message to a specific post data type.
/// </summary>
public T? AsPostData<T>() where T : class
{
if (MessageBytes == null) return null;
try
{
var msg = MessageBytes.MessagePackTo<SignalPostJsonDataMessage<T>>(ContractlessStandardResolver.Options);
return msg.PostData;
}
catch
{
// Fallback: try deserializing as raw JSON wrapper
try
{
var rawMsg = MessageBytes.MessagePackTo<SignalPostJsonMessage>(ContractlessStandardResolver.Options);
return rawMsg.PostDataJson?.JsonTo<T>();
}
catch
{
return null;
}
}
}
}
/// <summary>
/// Represents a message received by the client.
/// </summary>
public record ReceivedClientMessage(int MessageTag, byte[] MessageBytes)
{
/// <summary>
/// Deserializes the message as a response.
/// </summary>
public SignalResponseDataMessage? AsResponse()
{
try
{
return MessageBytes.BinaryTo<SignalResponseDataMessage>();
}
catch
{
return null;
}
}
}