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; /// /// Testable SignalR client that allows testing without real HubConnection. /// public class TestableSignalRClient : AcSignalRClientBase { private HubConnectionState _connectionState = HubConnectionState.Connected; private int? _nextRequestIdOverride; /// /// Messages sent to the server (captured for assertions). /// public List SentMessages { get; } = []; /// /// Received messages (captured for assertions). /// public List 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) /// /// Sets the simulated connection state. /// public void SetConnectionState(HubConnectionState state) => _connectionState = state; /// /// Sets the next request ID for deterministic testing. /// Will auto-increment for subsequent calls. /// public void SetNextRequestId(int id) => _nextRequestIdOverride = id; /// /// Gets the pending requests dictionary (public wrapper for testing). /// public new System.Collections.Concurrent.ConcurrentDictionary GetPendingRequests() => base.GetPendingRequests(); /// /// Registers a pending request (public wrapper for testing). /// public new void RegisterPendingRequest(int requestId, SignalRRequestModel model) => base.RegisterPendingRequest(requestId, model); /// /// Clears pending requests (public wrapper for testing). /// public new void ClearPendingRequests() => base.ClearPendingRequests(); /// /// Simulates receiving a response from the server. /// 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); } /// /// Simulates receiving a success response from the server. /// public Task SimulateSuccessResponse(int requestId, int messageTag, T data) => SimulateServerResponse(requestId, messageTag, SignalResponseStatus.Success, data); /// /// Simulates receiving an error response from the server. /// public Task SimulateErrorResponse(int requestId, int messageTag) => SimulateServerResponse(requestId, messageTag, SignalResponseStatus.Error); /// /// Gets the last sent message. /// public SentClientMessage? LastSentMessage => SentMessages.LastOrDefault(); /// /// Clears all captured messages. /// public void ClearMessages() { SentMessages.Clear(); ReceivedMessages.Clear(); } /// /// Invokes OnReceiveMessage directly for testing. /// public Task InvokeOnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId) => OnReceiveMessage(messageTag, messageBytes, requestId); #endregion } /// /// Represents a message sent from client to server. /// public record SentClientMessage(int MessageTag, byte[]? MessageBytes, int? RequestId) { /// /// Deserializes the message to IdMessage format. /// Works with both production SignalPostJsonDataMessage and test SignalRPostMessageDto. /// public IdMessage? AsIdMessage() { if (MessageBytes == null) return null; try { // First deserialize to get the PostDataJson string var msg = MessageBytes.MessagePackTo>(ContractlessStandardResolver.Options); return msg.PostData; } catch { // Fallback: try deserializing as raw JSON wrapper try { var rawMsg = MessageBytes.MessagePackTo(ContractlessStandardResolver.Options); return rawMsg.PostDataJson?.JsonTo(); } catch { return null; } } } /// /// Deserializes the message to a specific post data type. /// public T? AsPostData() where T : class { if (MessageBytes == null) return null; try { var msg = MessageBytes.MessagePackTo>(ContractlessStandardResolver.Options); return msg.PostData; } catch { // Fallback: try deserializing as raw JSON wrapper try { var rawMsg = MessageBytes.MessagePackTo(ContractlessStandardResolver.Options); return rawMsg.PostDataJson?.JsonTo(); } catch { return null; } } } } /// /// Represents a message received by the client. /// public record ReceivedClientMessage(int MessageTag, byte[] MessageBytes) { /// /// Deserializes the message as a response. /// public SignalResponseDataMessage? AsResponse() { try { return MessageBytes.BinaryTo(); } catch { return null; } } }