AyCode.Core/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub.cs

222 lines
6.2 KiB
C#

using System.Security.Claims;
using AyCode.Core.Extensions;
using AyCode.Core.Tests.TestModels;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
using Microsoft.Extensions.Configuration;
namespace AyCode.Services.Server.Tests.SignalRs;
/// <summary>
/// Testable SignalR hub that overrides infrastructure dependencies.
/// Enables unit testing without SignalR server or mocks.
/// </summary>
public class TestableSignalRHub : AcWebSignalRHubBase<TestSignalRTags, TestLogger>
{
#region Captured Data for Assertions
/// <summary>
/// Messages sent via ResponseToCaller or SendMessageToClient
/// </summary>
public List<SentMessage> SentMessages { get; } = [];
/// <summary>
/// Whether notFoundCallback was invoked
/// </summary>
public bool WasNotFoundCallbackInvoked { get; private set; }
/// <summary>
/// The tag name passed to notFoundCallback
/// </summary>
public string? NotFoundTagName { get; private set; }
#endregion
#region Test Configuration
/// <summary>
/// Simulated connection ID
/// </summary>
public string TestConnectionId { get; set; } = "test-connection-id";
/// <summary>
/// Simulated user identifier
/// </summary>
public string? TestUserIdentifier { get; set; } = "test-user-id";
/// <summary>
/// Simulated connection aborted state
/// </summary>
public bool TestIsConnectionAborted { get; set; } = false;
/// <summary>
/// Simulated ClaimsPrincipal (optional)
/// </summary>
public ClaimsPrincipal? TestUser { get; set; }
#endregion
public TestableSignalRHub()
: base(new ConfigurationBuilder().Build(), new TestLogger())
{
}
public TestableSignalRHub(IConfiguration configuration, TestLogger logger)
: base(configuration, logger)
{
}
#region Public Test Entry Points
/// <summary>
/// Sets the serializer type for testing (JSON or Binary).
/// </summary>
public void SetSerializerType(AcSerializerType serializerType)
{
SerializerOptions = serializerType == AcSerializerType.Binary
? new AcBinarySerializerOptions()
: new AcJsonSerializerOptions();
}
/// <summary>
/// Register a service with SignalR-attributed methods
/// </summary>
public void RegisterService(object service)
{
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(service));
}
/// <summary>
/// Invoke ProcessOnReceiveMessage for testing
/// </summary>
public Task InvokeProcessOnReceiveMessage(int messageTag, byte[]? message, int? requestId = null)
{
return ProcessOnReceiveMessage(messageTag, message, requestId, async tagName =>
{
WasNotFoundCallbackInvoked = true;
NotFoundTagName = tagName;
await Task.CompletedTask;
});
}
/// <summary>
/// Get the logger for assertions
/// </summary>
public new TestLogger Logger => base.Logger;
/// <summary>
/// Reset captured state for next test
/// </summary>
public void Reset()
{
SentMessages.Clear();
WasNotFoundCallbackInvoked = false;
NotFoundTagName = null;
Logger.Clear();
}
#endregion
#region Overridden Context Accessors
protected override string GetConnectionId() => TestConnectionId;
protected override bool IsConnectionAborted() => TestIsConnectionAborted;
protected override string? GetUserIdentifier() => TestUserIdentifier;
protected override ClaimsPrincipal? GetUser() => TestUser;
#endregion
#region Overridden Response Methods (capture messages for testing)
protected override Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId)
{
SentMessages.Add(new SentMessage(
MessageTag: messageTag,
Message: message,
RequestId: requestId,
Target: SendTarget.Caller
));
return Task.CompletedTask;
}
protected override Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
{
SentMessages.Add(new SentMessage(
MessageTag: messageTag,
Message: message,
RequestId: requestId,
Target: SendTarget.Client
));
return Task.CompletedTask;
}
protected override Task SendMessageToOthers(int messageTag, object? content)
{
SentMessages.Add(new SentMessage(
MessageTag: messageTag,
Message: new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content),
RequestId: null,
Target: SendTarget.Others
));
return Task.CompletedTask;
}
protected override Task SendMessageToAll(int messageTag, object? content)
{
SentMessages.Add(new SentMessage(
MessageTag: messageTag,
Message: new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, content),
RequestId: null,
Target: SendTarget.All
));
return Task.CompletedTask;
}
protected override Task SendMessageToUserIdInternal(string userId, int messageTag, ISignalRMessage message, int? requestId)
{
SentMessages.Add(new SentMessage(
MessageTag: messageTag,
Message: message,
RequestId: requestId,
Target: SendTarget.User,
TargetId: userId
));
return Task.CompletedTask;
}
#endregion
}
/// <summary>
/// Captured sent message for assertions
/// </summary>
public record SentMessage(
int MessageTag,
ISignalRMessage Message,
int? RequestId,
SendTarget Target,
string? TargetId = null)
{
/// <summary>
/// Get the response as SignalResponseJsonMessage for inspection
/// </summary>
public SignalResponseJsonMessage? AsJsonResponse => Message as SignalResponseJsonMessage;
}
/// <summary>
/// Target of the sent message
/// </summary>
public enum SendTarget
{
Caller,
Client,
Others,
All,
User,
Group
}