mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-04-28 03:07:01 +02:00
Version 2.x (#1359)
* Version 2.x * Setup .NET 9 * 12 * cleanup some #if for NETSTANDARD1_3 * cleanup + fix tests for net8 * openapi * NO ConfigureAwait(false) + cleanup * . * #endif * HashSet * WireMock.Net.NUnit * HttpContext * Add WebSockets (#1423) * Add WebSockets * Add tests * fix * more tests * Add tests * ... * remove IOwin * - * tests * fluent * ok * match * . * byte[] * x * func * func * byte * trans * ... * frameworks......... * jmes * xxx * sc * using var httpClient = new HttpClient(); * usings * maxRetries * up * xunit v3 * ct * --- * ct * ct2 * T Unit * WireMock.Net.TUnitTests / 10 * t unit first * --project * no tunit * t2 * --project * --project * ci - --project * publish ./test/wiremock-coverage.xml * windows * . * log * ... * log * goed * BodyType * . * . * --scenario * ... * pact * ct * . * WireMock.Net.RestClient.AwesomeAssertions (#1427) * WireMock.Net.RestClient.AwesomeAssertions * ok * atpath * fix test * sonar fixes * ports * proxy test * FIX? * --- * await Task.Delay(100, _ct); * ? * --project * Aspire: use IDistributedApplicationEventingSubscriber (#1428) * broadcast * ok * more tsts * . * Collection * up * . * 2 * remove nfluent * <VersionPrefix>2.0.0-preview-02</VersionPrefix> * ... * . * nuget icon * . * <PackageReference Include="JmesPath.Net" Version="1.1.0" /> * x * 500 * . * fix some warnings * ws
This commit is contained in:
280
src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs
Normal file
280
src/WireMock.Net.Minimal/WebSockets/WebSocketBuilder.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Net.WebSockets;
|
||||
using Stef.Validation;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Transformers;
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
internal class WebSocketBuilder(Response response) : IWebSocketBuilder
|
||||
{
|
||||
private readonly List<(IMatcher matcher, List<WebSocketMessageBuilder> messages)> _conditionalMessages = [];
|
||||
|
||||
public string? AcceptProtocol { get; private set; }
|
||||
|
||||
public bool IsEcho { get; private set; }
|
||||
|
||||
public Func<WebSocketMessage, IWebSocketContext, Task>? MessageHandler { get; private set; }
|
||||
|
||||
public ProxyAndRecordSettings? ProxySettings { get; private set; }
|
||||
|
||||
public TimeSpan? CloseTimeout { get; private set; }
|
||||
|
||||
public int? MaxMessageSize { get; private set; }
|
||||
|
||||
public int? ReceiveBufferSize { get; private set; }
|
||||
|
||||
public TimeSpan? KeepAliveIntervalSeconds { get; private set; }
|
||||
|
||||
public IWebSocketBuilder WithAcceptProtocol(string protocol)
|
||||
{
|
||||
AcceptProtocol = Guard.NotNull(protocol);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithEcho()
|
||||
{
|
||||
IsEcho = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder SendMessage(Action<IWebSocketMessageBuilder> configure)
|
||||
{
|
||||
Guard.NotNull(configure);
|
||||
var messageBuilder = new WebSocketMessageBuilder();
|
||||
configure(messageBuilder);
|
||||
|
||||
return WithMessageHandler(async (message, context) =>
|
||||
{
|
||||
if (messageBuilder.Delay.HasValue)
|
||||
{
|
||||
await Task.Delay(messageBuilder.Delay.Value);
|
||||
}
|
||||
|
||||
await SendMessageAsync(context, messageBuilder, message);
|
||||
});
|
||||
}
|
||||
|
||||
public IWebSocketBuilder SendMessages(Action<IWebSocketMessagesBuilder> configure)
|
||||
{
|
||||
Guard.NotNull(configure);
|
||||
var messagesBuilder = new WebSocketMessagesBuilder();
|
||||
configure(messagesBuilder);
|
||||
|
||||
return WithMessageHandler(async (message, context) =>
|
||||
{
|
||||
foreach (var messageBuilder in messagesBuilder.Messages)
|
||||
{
|
||||
if (messageBuilder.Delay.HasValue)
|
||||
{
|
||||
await Task.Delay(messageBuilder.Delay.Value);
|
||||
}
|
||||
|
||||
await SendMessageAsync(context, messageBuilder, message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public IWebSocketMessageConditionBuilder WhenMessage(string wildcardPattern)
|
||||
{
|
||||
Guard.NotNull(wildcardPattern);
|
||||
var matcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, wildcardPattern);
|
||||
return new WebSocketMessageConditionBuilder(this, matcher);
|
||||
}
|
||||
|
||||
public IWebSocketMessageConditionBuilder WhenMessage(byte[] exactPattern)
|
||||
{
|
||||
Guard.NotNull(exactPattern);
|
||||
var matcher = new ExactObjectMatcher(MatchBehaviour.AcceptOnMatch, exactPattern);
|
||||
return new WebSocketMessageConditionBuilder(this, matcher);
|
||||
}
|
||||
|
||||
public IWebSocketMessageConditionBuilder WhenMessage(IMatcher matcher)
|
||||
{
|
||||
Guard.NotNull(matcher);
|
||||
return new WebSocketMessageConditionBuilder(this, matcher);
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithMessageHandler(Func<WebSocketMessage, IWebSocketContext, Task> handler)
|
||||
{
|
||||
MessageHandler = Guard.NotNull(handler);
|
||||
IsEcho = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithProxy(ProxyAndRecordSettings settings)
|
||||
{
|
||||
ProxySettings = Guard.NotNull(settings);
|
||||
IsEcho = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithCloseTimeout(TimeSpan timeout)
|
||||
{
|
||||
CloseTimeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithMaxMessageSize(int sizeInBytes)
|
||||
{
|
||||
MaxMessageSize = Guard.Condition(sizeInBytes, s => s > 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithReceiveBufferSize(int sizeInBytes)
|
||||
{
|
||||
ReceiveBufferSize = Guard.Condition(sizeInBytes, s => s > 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWebSocketBuilder WithKeepAliveInterval(TimeSpan interval)
|
||||
{
|
||||
KeepAliveIntervalSeconds = interval;
|
||||
return this;
|
||||
}
|
||||
|
||||
internal IWebSocketBuilder AddConditionalMessage(IMatcher matcher, WebSocketMessageBuilder messageBuilder)
|
||||
{
|
||||
_conditionalMessages.Add((matcher, new List<WebSocketMessageBuilder> { messageBuilder }));
|
||||
SetupConditionalHandler();
|
||||
return this;
|
||||
}
|
||||
|
||||
internal IWebSocketBuilder AddConditionalMessages(IMatcher matcher, List<WebSocketMessageBuilder> messages)
|
||||
{
|
||||
_conditionalMessages.Add((matcher, messages));
|
||||
SetupConditionalHandler();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void SetupConditionalHandler()
|
||||
{
|
||||
if (_conditionalMessages.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WithMessageHandler(async (message, context) =>
|
||||
{
|
||||
// Check each condition in order
|
||||
foreach (var (matcher, messages) in _conditionalMessages)
|
||||
{
|
||||
// Try to match the message
|
||||
if (await MatchMessageAsync(message, matcher))
|
||||
{
|
||||
// Execute the corresponding messages
|
||||
foreach (var messageBuilder in messages)
|
||||
{
|
||||
if (messageBuilder.Delay.HasValue)
|
||||
{
|
||||
await Task.Delay(messageBuilder.Delay.Value);
|
||||
}
|
||||
|
||||
await SendMessageAsync(context, messageBuilder, message);
|
||||
|
||||
// If this message should close the connection, do it after sending
|
||||
if (messageBuilder.ShouldClose)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(100); // Small delay to ensure message is sent
|
||||
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by handler");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors during close
|
||||
}
|
||||
}
|
||||
}
|
||||
return; // Stop after first match
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync(IWebSocketContext context, WebSocketMessageBuilder messageBuilder, WebSocketMessage incomingMessage)
|
||||
{
|
||||
switch (messageBuilder.Type)
|
||||
{
|
||||
case WebSocketMessageType.Text:
|
||||
var text = messageBuilder.MessageText!;
|
||||
if (response.UseTransformer)
|
||||
{
|
||||
text = ApplyTransformer(context, incomingMessage, text);
|
||||
}
|
||||
await context.SendAsync(text);
|
||||
break;
|
||||
|
||||
case WebSocketMessageType.Binary:
|
||||
await context.SendAsync(messageBuilder.MessageBytes!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private string ApplyTransformer(IWebSocketContext context, WebSocketMessage incomingMessage, string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (incomingMessage == null)
|
||||
{
|
||||
// No incoming message, can't apply transformer
|
||||
return text;
|
||||
}
|
||||
|
||||
var transformer = TransformerFactory.Create(response.TransformerType, context.Mapping.Settings);
|
||||
|
||||
var model = new WebSocketTransformModel
|
||||
{
|
||||
Mapping = context.Mapping,
|
||||
Request = context.RequestMessage,
|
||||
Message = incomingMessage,
|
||||
Data = context.Mapping.Data
|
||||
};
|
||||
|
||||
return transformer.Transform(text, model);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If transformation fails, return original text
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> MatchMessageAsync(WebSocketMessage message, IMatcher matcher)
|
||||
{
|
||||
if (message.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
if (matcher is IStringMatcher stringMatcher)
|
||||
{
|
||||
var result = stringMatcher.IsMatch(message.Text);
|
||||
return result.IsPerfect();
|
||||
}
|
||||
|
||||
if (matcher is IFuncMatcher funcMatcher)
|
||||
{
|
||||
var result = funcMatcher.IsMatch(message.Text);
|
||||
return result.IsPerfect();
|
||||
}
|
||||
}
|
||||
|
||||
if (message.MessageType == WebSocketMessageType.Binary)
|
||||
{
|
||||
if (matcher is IBytesMatcher bytesMatcher)
|
||||
{
|
||||
var result = await bytesMatcher.IsMatchAsync(message.Bytes);
|
||||
return result.IsPerfect();
|
||||
}
|
||||
|
||||
if (matcher is IFuncMatcher funcMatcher)
|
||||
{
|
||||
var result = funcMatcher.IsMatch(message.Bytes);
|
||||
return result.IsPerfect();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
/// <summary>
|
||||
/// Registry for managing WebSocket connections per mapping
|
||||
/// </summary>
|
||||
internal class WebSocketConnectionRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, WireMockWebSocketContext> _connections = new();
|
||||
|
||||
/// <summary>
|
||||
/// Add a connection to the registry
|
||||
/// </summary>
|
||||
public void AddConnection(WireMockWebSocketContext context)
|
||||
{
|
||||
_connections.TryAdd(context.ConnectionId, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a connection from the registry
|
||||
/// </summary>
|
||||
public void RemoveConnection(Guid connectionId)
|
||||
{
|
||||
_ = _connections.TryRemove(connectionId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all connections
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<WireMockWebSocketContext> GetConnections()
|
||||
{
|
||||
return _connections.Values.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get a specific connection
|
||||
/// </summary>
|
||||
public bool TryGetConnection(Guid connectionId, [NotNullWhen(true)] out WireMockWebSocketContext? connection)
|
||||
{
|
||||
return _connections.TryGetValue(connectionId, out connection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast text to all connections
|
||||
/// </summary>
|
||||
public async Task BroadcastAsync(string text, Guid? excludeConnectionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = Filter(excludeConnectionId).Select(c => c.SendAsync(text, cancellationToken));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast binary to all connections
|
||||
/// </summary>
|
||||
public async Task BroadcastAsync(byte[] bytes, Guid? excludeConnectionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = Filter(excludeConnectionId).Select(c => c.SendAsync(bytes, cancellationToken));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private IEnumerable<WireMockWebSocketContext> Filter(Guid? excludeConnectionId)
|
||||
{
|
||||
return _connections.Values
|
||||
.Where(c => c.WebSocket.State == WebSocketState.Open && (!excludeConnectionId.HasValue || c.ConnectionId != excludeConnectionId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Net.WebSockets;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
internal class WebSocketMessageBuilder : IWebSocketMessageBuilder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string? MessageText { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[]? MessageBytes { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan? Delay { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public WebSocketMessageType Type { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldClose { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder WithEcho()
|
||||
{
|
||||
Type = WebSocketMessageType.Close;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder WithText(string text)
|
||||
{
|
||||
MessageText = Guard.NotNull(text);
|
||||
Type = WebSocketMessageType.Text;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder WithBinary(byte[] bytes)
|
||||
{
|
||||
MessageBytes = Guard.NotNull(bytes);
|
||||
Type = WebSocketMessageType.Binary;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder WithDelay(TimeSpan delay)
|
||||
{
|
||||
Delay = delay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder WithDelay(int delayInMilliseconds)
|
||||
{
|
||||
Guard.Condition(delayInMilliseconds, d => d >= 0);
|
||||
return WithDelay(TimeSpan.FromMilliseconds(delayInMilliseconds));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder Close()
|
||||
{
|
||||
ShouldClose = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocketMessageBuilder AndClose() => Close();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using WireMock.Matchers;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
internal class WebSocketMessageConditionBuilder(WebSocketBuilder parent, IMatcher matcher) : IWebSocketMessageConditionBuilder
|
||||
{
|
||||
public IWebSocketBuilder ThenSendMessage(Action<IWebSocketMessageBuilder> configure)
|
||||
{
|
||||
Guard.NotNull(configure);
|
||||
var messageBuilder = new WebSocketMessageBuilder();
|
||||
configure(messageBuilder);
|
||||
|
||||
return parent.AddConditionalMessage(matcher, messageBuilder);
|
||||
}
|
||||
|
||||
public IWebSocketBuilder SendMessages(Action<IWebSocketMessagesBuilder> configure)
|
||||
{
|
||||
Guard.NotNull(configure);
|
||||
var messagesBuilder = new WebSocketMessagesBuilder();
|
||||
configure(messagesBuilder);
|
||||
|
||||
return parent.AddConditionalMessages(matcher, messagesBuilder.Messages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the direction of a WebSocket message.
|
||||
/// </summary>
|
||||
internal enum WebSocketMessageDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Message received from the client.
|
||||
/// </summary>
|
||||
Receive,
|
||||
|
||||
/// <summary>
|
||||
/// Message sent to the client.
|
||||
/// </summary>
|
||||
Send
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
internal class WebSocketMessagesBuilder : IWebSocketMessagesBuilder
|
||||
{
|
||||
internal List<WebSocketMessageBuilder> Messages { get; } = [];
|
||||
|
||||
public IWebSocketMessagesBuilder AddMessage(Action<IWebSocketMessageBuilder> configure)
|
||||
{
|
||||
var messageBuilder = new WebSocketMessageBuilder();
|
||||
configure(messageBuilder);
|
||||
Messages.Add(messageBuilder);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
/// <summary>
|
||||
/// Model for WebSocket message transformation
|
||||
/// </summary>
|
||||
internal struct WebSocketTransformModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The mapping that matched this WebSocket request
|
||||
/// </summary>
|
||||
public IMapping Mapping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original request that initiated the WebSocket connection
|
||||
/// </summary>
|
||||
public IRequestMessage Request { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The incoming WebSocket message
|
||||
/// </summary>
|
||||
public WebSocketMessage Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mapping data as object
|
||||
/// </summary>
|
||||
public object? Data { get; set; }
|
||||
}
|
||||
251
src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs
Normal file
251
src/WireMock.Net.Minimal/WebSockets/WireMockWebSocketContext.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Models;
|
||||
using WireMock.Owin;
|
||||
using WireMock.Owin.ActivityTracing;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket context implementation
|
||||
/// </summary>
|
||||
public class WireMockWebSocketContext : IWebSocketContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid ConnectionId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpContext HttpContext { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public WebSocket WebSocket { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequestMessage RequestMessage { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IMapping Mapping { get; }
|
||||
|
||||
internal WebSocketConnectionRegistry Registry { get; }
|
||||
|
||||
internal WebSocketBuilder Builder { get; }
|
||||
|
||||
internal IWireMockMiddlewareOptions Options { get; }
|
||||
|
||||
internal IWireMockMiddlewareLogger Logger { get; }
|
||||
|
||||
internal WireMockWebSocketContext(
|
||||
HttpContext httpContext,
|
||||
WebSocket webSocket,
|
||||
IRequestMessage requestMessage,
|
||||
IMapping mapping,
|
||||
WebSocketConnectionRegistry registry,
|
||||
WebSocketBuilder builder,
|
||||
IWireMockMiddlewareOptions options,
|
||||
IWireMockMiddlewareLogger logger,
|
||||
IGuidUtils guidUtils
|
||||
)
|
||||
{
|
||||
HttpContext = httpContext;
|
||||
WebSocket = webSocket;
|
||||
RequestMessage = requestMessage;
|
||||
Mapping = mapping;
|
||||
Registry = registry;
|
||||
Builder = builder;
|
||||
Options = options;
|
||||
Logger = logger;
|
||||
|
||||
ConnectionId = guidUtils.NewGuid();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(string text, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
return SendAsyncInternal(
|
||||
new ArraySegment<byte>(bytes),
|
||||
WebSocketMessageType.Text,
|
||||
true,
|
||||
text,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(byte[] bytes, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsyncInternal(
|
||||
new ArraySegment<byte>(bytes),
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
bytes,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await WebSocket.CloseAsync(closeStatus, statusDescription, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
LogWebSocketMessage(WebSocketMessageDirection.Send, WebSocketMessageType.Close, $"CloseStatus: {closeStatus}, Description: {statusDescription}", null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Abort(string? statusDescription = null)
|
||||
{
|
||||
WebSocket.Abort();
|
||||
|
||||
LogWebSocketMessage(WebSocketMessageDirection.Send, WebSocketMessageType.Close, $"CloseStatus: Abort, Description: {statusDescription}", null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task BroadcastAsync(string text, bool excludeSender = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Guid? excludeConnectionId = excludeSender ? ConnectionId : null;
|
||||
await Registry.BroadcastAsync(text, excludeConnectionId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task BroadcastAsync(byte[] bytes, bool excludeSender = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Guid? excludeConnectionId = excludeSender ? ConnectionId : null;
|
||||
await Registry.BroadcastAsync(bytes, excludeConnectionId, cancellationToken);
|
||||
}
|
||||
|
||||
internal void LogWebSocketMessage(
|
||||
WebSocketMessageDirection direction,
|
||||
WebSocketMessageType messageType,
|
||||
object? data,
|
||||
Activity? activity)
|
||||
{
|
||||
IBodyData bodyData;
|
||||
if (messageType == WebSocketMessageType.Text && data is string textContent)
|
||||
{
|
||||
bodyData = new BodyData
|
||||
{
|
||||
BodyAsString = textContent,
|
||||
DetectedBodyType = BodyType.WebSocketText
|
||||
};
|
||||
}
|
||||
else if (messageType == WebSocketMessageType.Binary && data is byte[] binary)
|
||||
{
|
||||
bodyData = new BodyData
|
||||
{
|
||||
BodyAsBytes = binary,
|
||||
DetectedBodyType = BodyType.WebSocketBinary
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
bodyData = new BodyData
|
||||
{
|
||||
BodyAsString = messageType.ToString(),
|
||||
DetectedBodyType = BodyType.WebSocketClose
|
||||
};
|
||||
}
|
||||
|
||||
var method = $"WS_{direction.ToString().ToUpperInvariant()}";
|
||||
|
||||
RequestMessage? requestMessage = null;
|
||||
IResponseMessage? responseMessage = null;
|
||||
|
||||
if (direction == WebSocketMessageDirection.Receive)
|
||||
{
|
||||
// Received message - log as request
|
||||
requestMessage = new RequestMessage(
|
||||
new UrlDetails(RequestMessage.Url),
|
||||
method,
|
||||
RequestMessage.ClientIP,
|
||||
bodyData,
|
||||
null,
|
||||
null
|
||||
)
|
||||
{
|
||||
DateTime = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Sent message - log as response
|
||||
responseMessage = new ResponseMessage
|
||||
{
|
||||
Method = method,
|
||||
BodyData = bodyData,
|
||||
DateTime = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
// Create log entry
|
||||
var logEntry = new LogEntry
|
||||
{
|
||||
Guid = Guid.NewGuid(),
|
||||
RequestMessage = requestMessage,
|
||||
ResponseMessage = responseMessage,
|
||||
MappingGuid = Mapping.Guid,
|
||||
MappingTitle = Mapping.Title
|
||||
};
|
||||
|
||||
// Enrich activity if present
|
||||
if (activity != null && Options.ActivityTracingOptions != null)
|
||||
{
|
||||
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, Options.ActivityTracingOptions);
|
||||
}
|
||||
|
||||
// Log using LogLogEntry
|
||||
Logger.LogLogEntry(logEntry, Options.MaxRequestLogCount is null or > 0);
|
||||
|
||||
activity?.Dispose();
|
||||
}
|
||||
|
||||
private async Task SendAsyncInternal(
|
||||
ArraySegment<byte> buffer,
|
||||
WebSocketMessageType messageType,
|
||||
bool endOfMessage,
|
||||
object? data,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Activity? activity = null;
|
||||
var shouldTrace = Options.ActivityTracingOptions is not null;
|
||||
|
||||
if (shouldTrace)
|
||||
{
|
||||
activity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Send, Mapping.Guid);
|
||||
WireMockActivitySource.EnrichWithWebSocketMessage(
|
||||
activity,
|
||||
messageType,
|
||||
buffer.Count,
|
||||
endOfMessage,
|
||||
data as string,
|
||||
Options.ActivityTracingOptions
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await WebSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Log the send operation
|
||||
if (Options.MaxRequestLogCount is null or > 0)
|
||||
{
|
||||
LogWebSocketMessage(WebSocketMessageDirection.Send, messageType, data, activity);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WireMockActivitySource.RecordException(activity, ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
activity?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user