232 lines
7.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|