This commit is contained in:
Stef Heyenrath
2026-02-16 09:06:27 +01:00
parent 452dd2a529
commit bc15cfcefd
20 changed files with 799 additions and 215 deletions

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
namespace WireMock.Admin.Requests; namespace WireMock.Admin.Requests;
/// <summary> /// <summary>
@@ -17,12 +15,12 @@ public class LogEntryModel
/// <summary> /// <summary>
/// The request. /// The request.
/// </summary> /// </summary>
public required LogRequestModel Request { get; init; } public LogRequestModel? Request { get; init; }
/// <summary> /// <summary>
/// The response. /// The response.
/// </summary> /// </summary>
public required LogResponseModel Response { get; init; } public LogResponseModel? Response { get; init; }
/// <summary> /// <summary>
/// The mapping unique identifier. /// The mapping unique identifier.

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using WireMock.Admin.Mappings; using WireMock.Admin.Mappings;
using WireMock.Types; using WireMock.Types;

View File

@@ -64,12 +64,12 @@ public class LogResponseModel
/// <summary> /// <summary>
/// The detected body type (detection based on body content). /// The detected body type (detection based on body content).
/// </summary> /// </summary>
public BodyType? DetectedBodyType { get; set; } public string? DetectedBodyType { get; set; }
/// <summary> /// <summary>
/// The detected body type (detection based on Content-Type). /// The detected body type (detection based on Content-Type).
/// </summary> /// </summary>
public BodyType? DetectedBodyTypeFromContentType { get; set; } public string? DetectedBodyTypeFromContentType { get; set; }
/// <summary> /// <summary>
/// The FaultType. /// The FaultType.

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using WireMock.Types; using WireMock.Types;
using WireMock.Util; using WireMock.Util;

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Collections.Generic;
using WireMock.ResponseBuilders; using WireMock.ResponseBuilders;
using WireMock.Types; using WireMock.Types;
using WireMock.Util; using WireMock.Util;
@@ -52,6 +51,11 @@ public interface IResponseMessage
/// </summary> /// </summary>
object? StatusCode { get; set; } object? StatusCode { get; set; }
/// <summary>
/// Gets the DateTime.
/// </summary>
DateTime DateTime { get; }
/// <summary> /// <summary>
/// Adds the header. /// Adds the header.
/// </summary> /// </summary>

View File

@@ -38,20 +38,20 @@ public interface ILogEntry
/// <summary> /// <summary>
/// Gets the partial match result. /// Gets the partial match result.
/// </summary> /// </summary>
IRequestMatchResult PartialMatchResult { get; } IRequestMatchResult? PartialMatchResult { get; }
/// <summary> /// <summary>
/// Gets the request match result. /// Gets the request match result.
/// </summary> /// </summary>
IRequestMatchResult RequestMatchResult { get; } IRequestMatchResult? RequestMatchResult { get; }
/// <summary> /// <summary>
/// Gets the request message. /// Gets the request message.
/// </summary> /// </summary>
IRequestMessage RequestMessage { get; } IRequestMessage? RequestMessage { get; }
/// <summary> /// <summary>
/// Gets the response message. /// Gets the response message.
/// </summary> /// </summary>
IResponseMessage ResponseMessage { get; } IResponseMessage? ResponseMessage { get; }
} }

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using WireMock.Matchers.Request; using WireMock.Matchers.Request;
namespace WireMock.Logging; namespace WireMock.Logging;
@@ -14,13 +13,13 @@ public class LogEntry : ILogEntry
public Guid Guid { get; set; } public Guid Guid { get; set; }
/// <inheritdoc cref="ILogEntry.RequestMessage" /> /// <inheritdoc cref="ILogEntry.RequestMessage" />
public IRequestMessage RequestMessage { get; set; } = null!; public IRequestMessage? RequestMessage { get; set; }
/// <inheritdoc cref="ILogEntry.ResponseMessage" /> /// <inheritdoc cref="ILogEntry.ResponseMessage" />
public IResponseMessage ResponseMessage { get; set; } = null!; public IResponseMessage? ResponseMessage { get; set; }
/// <inheritdoc cref="ILogEntry.RequestMatchResult" /> /// <inheritdoc cref="ILogEntry.RequestMatchResult" />
public IRequestMatchResult RequestMatchResult { get; set; } = null!; public IRequestMatchResult? RequestMatchResult { get; set; }
/// <inheritdoc cref="ILogEntry.MappingGuid" /> /// <inheritdoc cref="ILogEntry.MappingGuid" />
public Guid? MappingGuid { get; set; } public Guid? MappingGuid { get; set; }
@@ -35,5 +34,5 @@ public class LogEntry : ILogEntry
public string? PartialMappingTitle { get; set; } public string? PartialMappingTitle { get; set; }
/// <inheritdoc cref="ILogEntry.PartialMatchResult" /> /// <inheritdoc cref="ILogEntry.PartialMatchResult" />
public IRequestMatchResult PartialMatchResult { get; set; } = null!; public IRequestMatchResult? PartialMatchResult { get; set; }
} }

View File

@@ -1,8 +1,10 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Diagnostics; using System.Diagnostics;
using System.Net.WebSockets;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Settings; using WireMock.Settings;
using WireMock.WebSockets;
namespace WireMock.Owin.ActivityTracing; namespace WireMock.Owin.ActivityTracing;
@@ -195,4 +197,59 @@ internal static class WireMockActivitySource
activity.SetTag("exception.message", exception.Message); activity.SetTag("exception.message", exception.Message);
activity.SetTag("exception.stacktrace", exception.ToString()); activity.SetTag("exception.stacktrace", exception.ToString());
} }
/// <summary>
/// Starts a new activity for a WebSocket message.
/// </summary>
/// <param name="direction">The direction of the message.</param>
/// <param name="mappingGuid">The GUID of the mapping handling the WebSocket.</param>
/// <returns>The started activity, or null if tracing is not enabled.</returns>
internal static Activity? StartWebSocketMessageActivity(WebSocketMessageDirection direction, Guid mappingGuid)
{
if (!Source.HasListeners())
{
return null;
}
var activity = Source.StartActivity(
$"WireMock WebSocket {direction.ToString().ToLowerInvariant()}",
ActivityKind.Server
);
if (activity != null)
{
activity.SetTag(WireMockSemanticConventions.MappingGuid, mappingGuid.ToString());
}
return activity;
}
/// <summary>
/// Enriches an activity with WebSocket message information.
/// </summary>
internal static void EnrichWithWebSocketMessage(
Activity? activity,
WebSocketMessageType messageType,
int messageSize,
bool endOfMessage,
string? textContent = null,
ActivityTracingOptions? options = null)
{
if (activity == null)
{
return;
}
activity.SetTag(WireMockSemanticConventions.WebSocketMessageType, messageType.ToString());
activity.SetTag(WireMockSemanticConventions.WebSocketMessageSize, messageSize);
activity.SetTag(WireMockSemanticConventions.WebSocketEndOfMessage, endOfMessage);
// Record message content if enabled and it's text
if (options?.RecordRequestBody == true && messageType == WebSocketMessageType.Text && textContent != null)
{
activity.SetTag(WireMockSemanticConventions.WebSocketMessageContent, textContent);
}
activity.SetTag("otel.status_code", "OK");
}
} }

View File

@@ -25,4 +25,10 @@ internal static class WireMockSemanticConventions
public const string RequestGuid = "wiremock.request.guid"; public const string RequestGuid = "wiremock.request.guid";
public const string RequestBody = "wiremock.request.body"; public const string RequestBody = "wiremock.request.body";
public const string ResponseBody = "wiremock.response.body"; public const string ResponseBody = "wiremock.response.body";
// WebSocket-specific attributes
public const string WebSocketMessageType = "wiremock.websocket.message.type";
public const string WebSocketMessageSize = "wiremock.websocket.message.size";
public const string WebSocketEndOfMessage = "wiremock.websocket.message.end_of_message";
public const string WebSocketMessageContent = "wiremock.websocket.message.content";
} }

View File

@@ -1,10 +1,13 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Diagnostics; using System.Diagnostics;
using WireMock.Logging;
namespace WireMock.Owin; namespace WireMock.Owin;
internal interface IWireMockMiddlewareLogger internal interface IWireMockMiddlewareLogger
{ {
void Log(bool logRequest, RequestMessage request, IResponseMessage? response, MappingMatcherResult? match, MappingMatcherResult? partialMatch, Activity? activity); void LogRequestAndResponse(bool logRequest, RequestMessage request, IResponseMessage? response, MappingMatcherResult? match, MappingMatcherResult? partialMatch, Activity? activity);
void LogLogEntry(LogEntry entry, bool addRequest);
} }

View File

@@ -42,7 +42,8 @@ internal class WireMockMiddleware(
private async Task InvokeInternalAsync(HttpContext ctx) private async Task InvokeInternalAsync(HttpContext ctx)
{ {
// Store options in HttpContext for providers to access (e.g., WebSocketResponseProvider) // Store options in HttpContext for providers to access (e.g., WebSocketResponseProvider)
ctx.Items[nameof(WireMockMiddlewareOptions)] = options; ctx.Items[nameof(IWireMockMiddlewareOptions)] = options;
ctx.Items[nameof(IWireMockMiddlewareLogger)] = logger;
var request = await requestMapper.MapAsync(ctx, options).ConfigureAwait(false); var request = await requestMapper.MapAsync(ctx, options).ConfigureAwait(false);
@@ -158,7 +159,7 @@ internal class WireMockMiddleware(
} }
finally finally
{ {
logger.Log(logRequest, request, response, result.Match, result.Partial, activity); logger.LogRequestAndResponse(logRequest, request, response, result.Match, result.Partial, activity);
try try
{ {

View File

@@ -2,43 +2,46 @@
using System.Diagnostics; using System.Diagnostics;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Matchers.Request;
using WireMock.Owin.ActivityTracing; using WireMock.Owin.ActivityTracing;
using WireMock.Serialization; using WireMock.Serialization;
using WireMock.Util; using WireMock.Util;
namespace WireMock.Owin; namespace WireMock.Owin;
internal class WireMockMiddlewareLogger(IWireMockMiddlewareOptions _options, LogEntryMapper _logEntryMapper, IGuidUtils _guidUtils) : IWireMockMiddlewareLogger internal class WireMockMiddlewareLogger(
IWireMockMiddlewareOptions _options,
LogEntryMapper _logEntryMapper,
IGuidUtils _guidUtils
) : IWireMockMiddlewareLogger
{ {
public void Log(bool logRequest, RequestMessage request, IResponseMessage? response, MappingMatcherResult? match, MappingMatcherResult? partialMatch, Activity? activity) public void LogRequestAndResponse(bool logRequest, RequestMessage request, IResponseMessage? response, MappingMatcherResult? match, MappingMatcherResult? partialMatch, Activity? activity)
{ {
var log = new LogEntry var logEntry = new LogEntry
{ {
Guid = _guidUtils.NewGuid(), Guid = _guidUtils.NewGuid(),
RequestMessage = request, RequestMessage = request,
ResponseMessage = response ?? new ResponseMessage(), ResponseMessage = response,
MappingGuid = match?.Mapping?.Guid, MappingGuid = match?.Mapping?.Guid,
MappingTitle = match?.Mapping?.Title, MappingTitle = match?.Mapping?.Title,
RequestMatchResult = match?.RequestMatchResult ?? new RequestMatchResult(), RequestMatchResult = match?.RequestMatchResult,
PartialMappingGuid = partialMatch?.Mapping?.Guid, PartialMappingGuid = partialMatch?.Mapping?.Guid,
PartialMappingTitle = partialMatch?.Mapping?.Title, PartialMappingTitle = partialMatch?.Mapping?.Title,
PartialMatchResult = partialMatch?.RequestMatchResult ?? new RequestMatchResult() PartialMatchResult = partialMatch?.RequestMatchResult
}; };
WireMockActivitySource.EnrichWithLogEntry(activity, log, _options.ActivityTracingOptions); WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, _options.ActivityTracingOptions);
activity?.Dispose(); activity?.Dispose();
LogRequest(log, logRequest); LogLogEntry(logEntry, logRequest);
try try
{ {
if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true }) if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
{ {
var filename = $"{log.Guid}.LogEntry.json"; var filename = $"{logEntry.Guid}.LogEntry.json";
_options.FileSystemHandler?.WriteUnmatchedRequest(filename, JsonUtils.Serialize(log)); _options.FileSystemHandler?.WriteUnmatchedRequest(filename, JsonUtils.Serialize(logEntry));
} }
} }
catch catch
@@ -47,21 +50,25 @@ internal class WireMockMiddlewareLogger(IWireMockMiddlewareOptions _options, Log
} }
} }
private void LogRequest(LogEntry entry, bool addRequest) public void LogLogEntry(LogEntry entry, bool addRequest)
{ {
_options.Logger.DebugRequestResponse(_logEntryMapper.Map(entry), entry.RequestMessage.Path.StartsWith("/__admin/")); if (entry.RequestMessage != null)
// If addRequest is set to true and MaxRequestLogCount is null or does have a value greater than 0, try to add a new request log.
if (addRequest && _options.MaxRequestLogCount is null or > 0)
{ {
TryAddLogEntry(entry); _options.Logger.DebugRequestResponse(_logEntryMapper.Map(entry), entry.RequestMessage.Path.StartsWith("/__admin/"));
// If addRequest is set to true and MaxRequestLogCount is null or does have a value greater than 0, try to add a new request log.
if (addRequest && _options.MaxRequestLogCount is null or > 0)
{
TryAddLogEntry(entry);
}
} }
// In case MaxRequestLogCount has a value greater than 0, try to delete existing request logs based on the count. // In case MaxRequestLogCount has a value greater than 0, try to delete existing request logs based on the count.
if (_options.MaxRequestLogCount is > 0) if (_options.MaxRequestLogCount is > 0)
{ {
var logEntries = _options.LogEntries.ToList(); var logEntries = _options.LogEntries.Where(le => le.RequestMessage != null).ToList();
foreach (var logEntry in logEntries.OrderBy(le => le.RequestMessage.DateTime).Take(logEntries.Count - _options.MaxRequestLogCount.Value))
foreach (var logEntry in logEntries.OrderBy(le => le.RequestMessage!.DateTime).Take(logEntries.Count - _options.MaxRequestLogCount.Value))
{ {
TryRemoveLogEntry(logEntry); TryRemoveLogEntry(logEntry);
} }
@@ -70,8 +77,10 @@ internal class WireMockMiddlewareLogger(IWireMockMiddlewareOptions _options, Log
// In case RequestLogExpirationDuration has a value greater than 0, try to delete existing request logs based on the date. // In case RequestLogExpirationDuration has a value greater than 0, try to delete existing request logs based on the date.
if (_options.RequestLogExpirationDuration is > 0) if (_options.RequestLogExpirationDuration is > 0)
{ {
var logEntries = _options.LogEntries.Where(le => le.RequestMessage != null).ToList();
var checkTime = DateTime.UtcNow.AddHours(-_options.RequestLogExpirationDuration.Value); var checkTime = DateTime.UtcNow.AddHours(-_options.RequestLogExpirationDuration.Value);
foreach (var logEntry in _options.LogEntries.ToList().Where(le => le.RequestMessage.DateTime < checkTime)) foreach (var logEntry in logEntries.Where(le => le.RequestMessage!.DateTime < checkTime))
{ {
TryRemoveLogEntry(logEntry); TryRemoveLogEntry(logEntry);
} }

View File

@@ -2,13 +2,10 @@
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
// For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root.
using System.Collections.Generic; using Stef.Validation;
using System.Linq;
using WireMock.ResponseBuilders; using WireMock.ResponseBuilders;
using WireMock.Types; using WireMock.Types;
using WireMock.Util; using WireMock.Util;
using Stef.Validation;
using WireMock.WebSockets;
namespace WireMock; namespace WireMock;
@@ -41,6 +38,9 @@ public class ResponseMessage : IResponseMessage
/// <inheritdoc cref="IResponseMessage.FaultPercentage" /> /// <inheritdoc cref="IResponseMessage.FaultPercentage" />
public double? FaultPercentage { get; set; } public double? FaultPercentage { get; set; }
/// <inheritdoc />
public DateTime DateTime { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public void AddHeader(string name, string value) public void AddHeader(string name, string value)
{ {

View File

@@ -1,13 +1,22 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Buffers; using System.Buffers;
using System.Diagnostics;
using System.Drawing;
using System.Net; using System.Net;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using WireMock.Constants; using WireMock.Constants;
using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.Owin; using WireMock.Owin;
using WireMock.Owin.ActivityTracing;
using WireMock.Settings; using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
using WireMock.WebSockets; using WireMock.WebSockets;
namespace WireMock.ResponseProviders; namespace WireMock.ResponseProviders;
@@ -42,10 +51,17 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
#endif #endif
// Get options from HttpContext.Items (set by WireMockMiddleware) // Get options from HttpContext.Items (set by WireMockMiddleware)
if (!context.Items.TryGetValue(nameof(WireMockMiddlewareOptions), out var optionsObj) || if (!context.Items.TryGetValue(nameof(IWireMockMiddlewareOptions), out var optionsObj) ||
optionsObj is not IWireMockMiddlewareOptions options) optionsObj is not IWireMockMiddlewareOptions options)
{ {
throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items"); throw new InvalidOperationException("IWireMockMiddlewareOptions not found in HttpContext.Items");
}
// Get logger from HttpContext.Items
if (!context.Items.TryGetValue(nameof(IWireMockMiddlewareLogger), out var loggerObj) ||
loggerObj is not IWireMockMiddlewareLogger logger)
{
throw new InvalidOperationException("IWireMockMiddlewareLogger not found in HttpContext.Items");
} }
// Get or create registry from options // Get or create registry from options
@@ -60,7 +76,9 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
requestMessage, requestMessage,
mapping, mapping,
registry, registry,
builder builder,
options,
logger
); );
// Update scenario state following the same pattern as WireMockMiddleware // Update scenario state following the same pattern as WireMockMiddleware
@@ -124,31 +142,87 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes); var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
using var cts = new CancellationTokenSource(timeout); using var cts = new CancellationTokenSource(timeout);
var shouldTrace = context.Options?.ActivityTracingOptions is not null;
try try
{ {
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested) while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
{ {
var result = await context.WebSocket.ReceiveAsync( Activity? receiveActivity = null;
new ArraySegment<byte>(buffer), if (shouldTrace)
cts.Token
).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Close)
{ {
await context.CloseAsync( receiveActivity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Receive, context.Mapping.Guid);
WebSocketCloseStatus.NormalClosure,
"Closed by client"
).ConfigureAwait(false);
break;
} }
// Echo back try
await context.WebSocket.SendAsync( {
new ArraySegment<byte>(buffer, 0, result.Count), var result = await context.WebSocket.ReceiveAsync(
result.MessageType, new ArraySegment<byte>(buffer),
result.EndOfMessage, cts.Token
cts.Token ).ConfigureAwait(false);
).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Close)
{
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
receiveActivity,
result.MessageType,
result.Count,
result.EndOfMessage,
null,
context.Options?.ActivityTracingOptions
);
}
LogWebSocketMessage(context, WebSocketMessageDirection.Receive, result.MessageType, null, receiveActivity);
await context.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Closed by client"
).ConfigureAwait(false);
break;
}
// Enrich activity with message details
string? textContent = null;
if (result.MessageType == WebSocketMessageType.Text)
{
textContent = Encoding.UTF8.GetString(buffer, 0, result.Count);
}
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
receiveActivity,
result.MessageType,
result.Count,
result.EndOfMessage,
textContent,
context.Options?.ActivityTracingOptions
);
}
// Log the receive operation
LogWebSocketMessage(context, WebSocketMessageDirection.Receive, result.MessageType, textContent, receiveActivity);
// Echo back (this will be logged by context.SendAsync)
await context.WebSocket.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count),
result.MessageType,
result.EndOfMessage,
cts.Token
).ConfigureAwait(false);
}
catch (Exception ex)
{
WireMockActivitySource.RecordException(receiveActivity, ex);
throw;
}
finally
{
receiveActivity?.Dispose();
}
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -169,28 +243,79 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes); var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
using var cts = new CancellationTokenSource(timeout); using var cts = new CancellationTokenSource(timeout);
var shouldTrace = context.Options?.ActivityTracingOptions is not null;
try try
{ {
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested) while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
{ {
var result = await context.WebSocket.ReceiveAsync( Activity? receiveActivity = null;
new ArraySegment<byte>(buffer), if (shouldTrace)
cts.Token
).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Close)
{ {
await context.CloseAsync( receiveActivity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Receive, context.Mapping.Guid);
WebSocketCloseStatus.NormalClosure,
"Closed by client"
).ConfigureAwait(false);
break;
} }
var message = CreateWebSocketMessage(result, buffer); try
{
var result = await context.WebSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
cts.Token
).ConfigureAwait(false);
// Call custom handler if (result.MessageType == WebSocketMessageType.Close)
await handler(message, context).ConfigureAwait(false); {
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
receiveActivity,
result.MessageType,
result.Count,
result.EndOfMessage,
null,
context.Options?.ActivityTracingOptions
);
}
LogWebSocketMessage(context, WebSocketMessageDirection.Receive, result.MessageType, null, receiveActivity);
await context.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Closed by client"
).ConfigureAwait(false);
break;
}
var message = CreateWebSocketMessage(result, buffer);
// Enrich activity with message details
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
receiveActivity,
result.MessageType,
result.Count,
result.EndOfMessage,
message.Text,
context.Options?.ActivityTracingOptions
);
}
// Log the receive operation
object? data = message.Text != null ? message.Text : message.Bytes;
LogWebSocketMessage(context, WebSocketMessageDirection.Receive, result.MessageType, data, receiveActivity);
// Call custom handler
await handler(message, context).ConfigureAwait(false);
}
catch (Exception ex)
{
WireMockActivitySource.RecordException(receiveActivity, ex);
throw;
}
finally
{
receiveActivity?.Dispose();
}
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -210,8 +335,8 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
await clientWebSocket.ConnectAsync(targetUri, CancellationToken.None).ConfigureAwait(false); await clientWebSocket.ConnectAsync(targetUri, CancellationToken.None).ConfigureAwait(false);
// Bidirectional proxy // Bidirectional proxy
var clientToServer = ForwardMessagesAsync(context.WebSocket, clientWebSocket); var clientToServer = ForwardMessagesAsync(context, clientWebSocket, WebSocketMessageDirection.Receive);
var serverToClient = ForwardMessagesAsync(clientWebSocket, context.WebSocket); var serverToClient = ForwardMessagesAsync(context, clientWebSocket, WebSocketMessageDirection.Send);
await Task.WhenAny(clientToServer, serverToClient).ConfigureAwait(false); await Task.WhenAny(clientToServer, serverToClient).ConfigureAwait(false);
@@ -227,30 +352,103 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
} }
} }
private static async Task ForwardMessagesAsync(WebSocket source, WebSocket destination) private static async Task ForwardMessagesAsync(
WireMockWebSocketContext context,
ClientWebSocket clientWebSocket,
WebSocketMessageDirection direction)
{ {
var buffer = new byte[WebSocketConstants.ProxyForwardBufferSize]; var buffer = new byte[WebSocketConstants.ProxyForwardBufferSize];
// Get options for activity tracing
var options = context.HttpContext.Items.TryGetValue(nameof(WireMockMiddlewareOptions), out var optionsObj) &&
optionsObj is IWireMockMiddlewareOptions wireMockOptions
? wireMockOptions
: null;
var shouldTrace = options?.ActivityTracingOptions is not null;
var source = direction == WebSocketMessageDirection.Receive ? context.WebSocket : (WebSocket)clientWebSocket;
var destination = direction == WebSocketMessageDirection.Receive ? (WebSocket)clientWebSocket : context.WebSocket;
while (source.State == WebSocketState.Open && destination.State == WebSocketState.Open) while (source.State == WebSocketState.Open && destination.State == WebSocketState.Open)
{ {
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); Activity? activity = null;
if (shouldTrace)
if (result.MessageType == WebSocketMessageType.Close)
{ {
await destination.CloseAsync( activity = WireMockActivitySource.StartWebSocketMessageActivity(direction, context.Mapping.Guid);
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
result.CloseStatusDescription,
CancellationToken.None
);
break;
} }
await destination.SendAsync( try
new ArraySegment<byte>(buffer, 0, result.Count), {
result.MessageType, var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
result.EndOfMessage,
CancellationToken.None if (result.MessageType == WebSocketMessageType.Close)
); {
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
activity,
result.MessageType,
result.Count,
result.EndOfMessage,
null,
options?.ActivityTracingOptions
);
}
LogWebSocketMessage(context, direction, result.MessageType, null, activity);
await destination.CloseAsync(
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
result.CloseStatusDescription,
CancellationToken.None
);
break;
}
// Enrich activity with message details
object? data = null;
if (result.MessageType == WebSocketMessageType.Text)
{
data = Encoding.UTF8.GetString(buffer, 0, result.Count);
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
data = new byte[result.Count];
Array.Copy(buffer, (byte[])data, result.Count);
}
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
activity,
result.MessageType,
result.Count,
result.EndOfMessage,
data as string,
options?.ActivityTracingOptions
);
}
// Log the proxy operation
LogWebSocketMessage(context, direction, result.MessageType, data, activity);
await destination.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count),
result.MessageType,
result.EndOfMessage,
CancellationToken.None
);
}
catch (Exception ex)
{
WireMockActivitySource.RecordException(activity, ex);
throw;
}
finally
{
activity?.Dispose();
}
} }
} }
@@ -260,19 +458,65 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes); var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
using var cts = new CancellationTokenSource(timeout); using var cts = new CancellationTokenSource(timeout);
var shouldTrace = context.Options?.ActivityTracingOptions is not null;
try try
{ {
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested) while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
{ {
var result = await context.WebSocket.ReceiveAsync( Activity? receiveActivity = null;
new ArraySegment<byte>(buffer), if (shouldTrace)
cts.Token
);
if (result.MessageType == WebSocketMessageType.Close)
{ {
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client"); receiveActivity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Receive, context.Mapping.Guid);
break; }
try
{
var result = await context.WebSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
cts.Token
);
if (shouldTrace)
{
WireMockActivitySource.EnrichWithWebSocketMessage(
receiveActivity,
result.MessageType,
result.Count,
result.EndOfMessage,
null,
context.Options?.ActivityTracingOptions
);
}
// Log the receive operation
object? data = null;
if (result.MessageType == WebSocketMessageType.Text)
{
data = Encoding.UTF8.GetString(buffer, 0, result.Count);
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
data = new byte[result.Count];
Array.Copy(buffer, (byte[])data, result.Count);
}
LogWebSocketMessage(context, WebSocketMessageDirection.Receive, result.MessageType, data, receiveActivity);
if (result.MessageType == WebSocketMessageType.Close)
{
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client");
break;
}
}
catch (Exception ex)
{
WireMockActivitySource.RecordException(receiveActivity, ex);
throw;
}
finally
{
receiveActivity?.Dispose();
} }
} }
} }
@@ -285,6 +529,105 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
} }
} }
private static void LogWebSocketMessage(
WireMockWebSocketContext context,
WebSocketMessageDirection direction,
WebSocketMessageType messageType,
object? data,
Activity? activity)
{
// Skip logging if log count limit is disabled
if (context.Options.MaxRequestLogCount == 0)
{
return;
}
// Create body data
IBodyData bodyData;
if (messageType == WebSocketMessageType.Text && data is string textContent)
{
bodyData = new BodyData
{
BodyAsString = textContent,
DetectedBodyType = BodyType.String
};
}
else if (messageType == WebSocketMessageType.Binary && data is byte[] binary)
{
bodyData = new BodyData
{
BodyAsBytes = binary,
DetectedBodyType = BodyType.Bytes
};
}
else
{
bodyData = new BodyData
{
BodyAsString = messageType.ToString(),
DetectedBodyType = BodyType.Bytes
};
}
// Create a pseudo-request or pseudo-response depending on direction
RequestMessage? requestMessage = null;
IResponseMessage? responseMessage = null;
var method = $"WS_{direction.ToString().ToUpperInvariant()}";
if (direction == WebSocketMessageDirection.Receive)
{
// Received message - log as request
requestMessage = new RequestMessage(
new UrlDetails(context.RequestMessage.Url),
method,
context.RequestMessage.ClientIP,
bodyData,
null,
null
)
{
DateTime = DateTime.UtcNow
};
}
else
{
// Sent message - log as response
responseMessage = new ResponseMessage
{
StatusCode = HttpStatusCode.SwitchingProtocols, // WebSocket status
BodyData = bodyData,
DateTime = DateTime.UtcNow
};
}
// Create a perfect match result
var requestMatchResult = new RequestMatchResult();
requestMatchResult.AddScore(typeof(WebSocketMessageDirection), MatchScores.Perfect, null);
// Create log entry
var logEntry = new LogEntry
{
Guid = Guid.NewGuid(),
RequestMessage = requestMessage,
ResponseMessage = responseMessage,
MappingGuid = context.Mapping.Guid,
MappingTitle = context.Mapping.Title,
RequestMatchResult = requestMatchResult
};
// Enrich activity if present
if (activity != null && context.Options.ActivityTracingOptions != null)
{
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, context.Options.ActivityTracingOptions);
}
// Log using LogLogEntry
context.Logger.LogLogEntry(logEntry, context.Options.MaxRequestLogCount is null or > 0);
activity?.Dispose();
}
private static WebSocketMessage CreateWebSocketMessage(WebSocketReceiveResult result, byte[] buffer) private static WebSocketMessage CreateWebSocketMessage(WebSocketReceiveResult result, byte[] buffer)
{ {
var message = new WebSocketMessage var message = new WebSocketMessage

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using Stef.Validation;
using WireMock.Admin.Mappings; using WireMock.Admin.Mappings;
using WireMock.Admin.Requests; using WireMock.Admin.Requests;
using WireMock.Logging; using WireMock.Logging;
@@ -11,95 +10,96 @@ using WireMock.Types;
namespace WireMock.Serialization; namespace WireMock.Serialization;
internal class LogEntryMapper internal class LogEntryMapper(IWireMockMiddlewareOptions options)
{ {
private readonly IWireMockMiddlewareOptions _options;
public LogEntryMapper(IWireMockMiddlewareOptions options)
{
_options = Guard.NotNull(options);
}
public LogEntryModel Map(ILogEntry logEntry) public LogEntryModel Map(ILogEntry logEntry)
{ {
var logRequestModel = new LogRequestModel LogRequestModel? logRequestModel = null;
if (logEntry.RequestMessage != null)
{ {
DateTime = logEntry.RequestMessage.DateTime, logRequestModel = new LogRequestModel
ClientIP = logEntry.RequestMessage.ClientIP,
Path = logEntry.RequestMessage.Path,
AbsolutePath = logEntry.RequestMessage.AbsolutePath,
Url = logEntry.RequestMessage.Url,
AbsoluteUrl = logEntry.RequestMessage.AbsoluteUrl,
ProxyUrl = logEntry.RequestMessage.ProxyUrl,
Query = logEntry.RequestMessage.Query,
Method = logEntry.RequestMessage.Method,
HttpVersion = logEntry.RequestMessage.HttpVersion,
Headers = logEntry.RequestMessage.Headers,
Cookies = logEntry.RequestMessage.Cookies
};
if (logEntry.RequestMessage.BodyData != null)
{
logRequestModel.DetectedBodyType = logEntry.RequestMessage.BodyData.DetectedBodyType?.ToString();
logRequestModel.DetectedBodyTypeFromContentType = logEntry.RequestMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
switch (logEntry.RequestMessage.BodyData.DetectedBodyType)
{ {
case BodyType.String: DateTime = logEntry.RequestMessage.DateTime,
case BodyType.FormUrlEncoded: ClientIP = logEntry.RequestMessage.ClientIP,
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString; Path = logEntry.RequestMessage.Path,
break; AbsolutePath = logEntry.RequestMessage.AbsolutePath,
Url = logEntry.RequestMessage.Url,
AbsoluteUrl = logEntry.RequestMessage.AbsoluteUrl,
ProxyUrl = logEntry.RequestMessage.ProxyUrl,
Query = logEntry.RequestMessage.Query,
Method = logEntry.RequestMessage.Method,
HttpVersion = logEntry.RequestMessage.HttpVersion,
Headers = logEntry.RequestMessage.Headers,
Cookies = logEntry.RequestMessage.Cookies
};
case BodyType.Json: if (logEntry.RequestMessage.BodyData != null)
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString; // In case of Json, do also save the Body as string (backwards compatible) {
logRequestModel.BodyAsJson = logEntry.RequestMessage.BodyData.BodyAsJson; logRequestModel.DetectedBodyType = logEntry.RequestMessage.BodyData.DetectedBodyType?.ToString();
break; logRequestModel.DetectedBodyTypeFromContentType = logEntry.RequestMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
case BodyType.Bytes: switch (logEntry.RequestMessage.BodyData.DetectedBodyType)
logRequestModel.BodyAsBytes = logEntry.RequestMessage.BodyData.BodyAsBytes; {
break; case BodyType.String:
case BodyType.FormUrlEncoded:
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString;
break;
case BodyType.Json:
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString; // In case of Json, do also save the Body as string (backwards compatible)
logRequestModel.BodyAsJson = logEntry.RequestMessage.BodyData.BodyAsJson;
break;
case BodyType.Bytes:
logRequestModel.BodyAsBytes = logEntry.RequestMessage.BodyData.BodyAsBytes;
break;
}
logRequestModel.BodyEncoding = logEntry.RequestMessage.BodyData.Encoding != null
? new EncodingModel
{
EncodingName = logEntry.RequestMessage.BodyData.Encoding.EncodingName,
CodePage = logEntry.RequestMessage.BodyData.Encoding.CodePage,
WebName = logEntry.RequestMessage.BodyData.Encoding.WebName
}
: null;
}
}
LogResponseModel? logResponseModel = null;
if (logEntry.ResponseMessage != null)
{
logResponseModel = new LogResponseModel
{
StatusCode = logEntry.ResponseMessage.StatusCode,
Headers = logEntry.ResponseMessage.Headers
};
if (logEntry.ResponseMessage.FaultType != FaultType.NONE)
{
logResponseModel.FaultType = logEntry.ResponseMessage.FaultType.ToString();
logResponseModel.FaultPercentage = logEntry.ResponseMessage.FaultPercentage;
} }
logRequestModel.BodyEncoding = logEntry.RequestMessage.BodyData.Encoding != null if (logEntry.ResponseMessage.BodyData != null)
? new EncodingModel {
{ logResponseModel.BodyOriginal = logEntry.ResponseMessage.BodyOriginal;
EncodingName = logEntry.RequestMessage.BodyData.Encoding.EncodingName, logResponseModel.BodyDestination = logEntry.ResponseMessage.BodyDestination;
CodePage = logEntry.RequestMessage.BodyData.Encoding.CodePage,
WebName = logEntry.RequestMessage.BodyData.Encoding.WebName
}
: null;
}
var logResponseModel = new LogResponseModel logResponseModel.DetectedBodyType = logEntry.ResponseMessage.BodyData.DetectedBodyType?.ToString();
{ logResponseModel.DetectedBodyTypeFromContentType = logEntry.ResponseMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
StatusCode = logEntry.ResponseMessage.StatusCode,
Headers = logEntry.ResponseMessage.Headers
};
if (logEntry.ResponseMessage.FaultType != FaultType.NONE) MapBody(logEntry, logResponseModel);
{
logResponseModel.FaultType = logEntry.ResponseMessage.FaultType.ToString();
logResponseModel.FaultPercentage = logEntry.ResponseMessage.FaultPercentage;
}
if (logEntry.ResponseMessage.BodyData != null) logResponseModel.BodyEncoding = logEntry.ResponseMessage.BodyData.Encoding != null
{ ? new EncodingModel
logResponseModel.BodyOriginal = logEntry.ResponseMessage.BodyOriginal; {
logResponseModel.BodyDestination = logEntry.ResponseMessage.BodyDestination; EncodingName = logEntry.ResponseMessage.BodyData.Encoding.EncodingName,
CodePage = logEntry.ResponseMessage.BodyData.Encoding.CodePage,
logResponseModel.DetectedBodyType = logEntry.ResponseMessage.BodyData.DetectedBodyType; WebName = logEntry.ResponseMessage.BodyData.Encoding.WebName
logResponseModel.DetectedBodyTypeFromContentType = logEntry.ResponseMessage.BodyData.DetectedBodyTypeFromContentType; }
: null;
MapBody(logEntry, logResponseModel); }
logResponseModel.BodyEncoding = logEntry.ResponseMessage.BodyData.Encoding != null
? new EncodingModel
{
EncodingName = logEntry.ResponseMessage.BodyData.Encoding.EncodingName,
CodePage = logEntry.ResponseMessage.BodyData.Encoding.CodePage,
WebName = logEntry.ResponseMessage.BodyData.Encoding.WebName
}
: null;
} }
return new LogEntryModel return new LogEntryModel
@@ -120,11 +120,11 @@ internal class LogEntryMapper
private void MapBody(ILogEntry logEntry, LogResponseModel logResponseModel) private void MapBody(ILogEntry logEntry, LogResponseModel logResponseModel)
{ {
switch (logEntry.ResponseMessage.BodyData!.DetectedBodyType) switch (logEntry.ResponseMessage?.BodyData?.DetectedBodyType)
{ {
case BodyType.String: case BodyType.String:
case BodyType.FormUrlEncoded: case BodyType.FormUrlEncoded:
if (!string.IsNullOrEmpty(logEntry.ResponseMessage.BodyData.IsFuncUsed) && _options.DoNotSaveDynamicResponseInLogEntry == true) if (!string.IsNullOrEmpty(logEntry.ResponseMessage.BodyData.IsFuncUsed) && options.DoNotSaveDynamicResponseInLogEntry == true)
{ {
logResponseModel.Body = logEntry.ResponseMessage.BodyData.IsFuncUsed; logResponseModel.Body = logEntry.ResponseMessage.BodyData.IsFuncUsed;
} }

View File

@@ -1,10 +1,6 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using WireMock.WebSockets; using WireMock.WebSockets;

View File

@@ -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
}

View File

@@ -1,11 +1,20 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Stef.Validation; using Stef.Validation;
using WireMock.Extensions; using WireMock.Extensions;
using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.Owin; using WireMock.Owin;
using WireMock.Owin.ActivityTracing;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.WebSockets; namespace WireMock.WebSockets;
@@ -14,8 +23,6 @@ namespace WireMock.WebSockets;
/// </summary> /// </summary>
public class WireMockWebSocketContext : IWebSocketContext public class WireMockWebSocketContext : IWebSocketContext
{ {
private readonly IWireMockMiddlewareOptions _options;
/// <inheritdoc /> /// <inheritdoc />
public Guid ConnectionId { get; } = Guid.NewGuid(); public Guid ConnectionId { get; } = Guid.NewGuid();
@@ -35,6 +42,10 @@ public class WireMockWebSocketContext : IWebSocketContext
internal WebSocketBuilder Builder { get; } internal WebSocketBuilder Builder { get; }
internal IWireMockMiddlewareOptions Options { get; }
internal IWireMockMiddlewareLogger Logger { get; }
/// <summary> /// <summary>
/// Creates a new WebSocketContext /// Creates a new WebSocketContext
/// </summary> /// </summary>
@@ -44,7 +55,9 @@ public class WireMockWebSocketContext : IWebSocketContext
IRequestMessage requestMessage, IRequestMessage requestMessage,
IMapping mapping, IMapping mapping,
WebSocketConnectionRegistry? registry, WebSocketConnectionRegistry? registry,
WebSocketBuilder builder) WebSocketBuilder builder,
IWireMockMiddlewareOptions options,
IWireMockMiddlewareLogger logger)
{ {
HttpContext = Guard.NotNull(httpContext); HttpContext = Guard.NotNull(httpContext);
WebSocket = Guard.NotNull(webSocket); WebSocket = Guard.NotNull(webSocket);
@@ -52,26 +65,19 @@ public class WireMockWebSocketContext : IWebSocketContext
Mapping = Guard.NotNull(mapping); Mapping = Guard.NotNull(mapping);
Registry = registry; Registry = registry;
Builder = Guard.NotNull(builder); Builder = Guard.NotNull(builder);
Options = Guard.NotNull(options);
// Get options from HttpContext Logger = Guard.NotNull(logger);
if (httpContext.Items.TryGetValue<IWireMockMiddlewareOptions>(nameof(WireMockMiddlewareOptions), out var options))
{
_options = options;
}
else
{
throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items");
}
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SendAsync(string text, CancellationToken cancellationToken = default) public Task SendAsync(string text, CancellationToken cancellationToken = default)
{ {
var bytes = Encoding.UTF8.GetBytes(text); var bytes = Encoding.UTF8.GetBytes(text);
return WebSocket.SendAsync( return SendAsyncInternal(
new ArraySegment<byte>(bytes), new ArraySegment<byte>(bytes),
WebSocketMessageType.Text, WebSocketMessageType.Text,
true, true,
text,
cancellationToken cancellationToken
); );
} }
@@ -79,18 +85,157 @@ public class WireMockWebSocketContext : IWebSocketContext
/// <inheritdoc /> /// <inheritdoc />
public Task SendAsync(byte[] bytes, CancellationToken cancellationToken = default) public Task SendAsync(byte[] bytes, CancellationToken cancellationToken = default)
{ {
return WebSocket.SendAsync( return SendAsyncInternal(
new ArraySegment<byte>(bytes), new ArraySegment<byte>(bytes),
WebSocketMessageType.Binary, WebSocketMessageType.Binary,
true, true,
bytes,
cancellationToken cancellationToken
); );
} }
/// <inheritdoc /> private async Task SendAsyncInternal(
public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription) ArraySegment<byte> buffer,
WebSocketMessageType messageType,
bool endOfMessage,
object? data,
CancellationToken cancellationToken)
{ {
return WebSocket.CloseAsync(closeStatus, statusDescription, CancellationToken.None); 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();
}
}
private void LogWebSocketMessage(
WebSocketMessageDirection direction,
WebSocketMessageType messageType,
object? data,
Activity? activity)
{
// Create body data
IBodyData bodyData;
if (messageType == WebSocketMessageType.Text && data is string textContent)
{
bodyData = new BodyData
{
BodyAsString = textContent,
DetectedBodyType = BodyType.String
};
}
else if (messageType == WebSocketMessageType.Binary && data is byte[] binary)
{
bodyData = new BodyData
{
BodyAsBytes = binary,
DetectedBodyType = BodyType.Bytes
};
}
else
{
bodyData = new BodyData
{
BodyAsString = messageType.ToString(),
DetectedBodyType = BodyType.Bytes
};
}
// Create a pseudo-request or pseudo-response depending on direction
RequestMessage? requestMessage = null;
IResponseMessage? responseMessage = null;
var method = $"WS_{direction.ToString().ToUpperInvariant()}";
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
{
StatusCode = HttpStatusCode.SwitchingProtocols, // WebSocket status
BodyData = bodyData,
DateTime = DateTime.UtcNow
};
}
// Create a perfect match result
var requestMatchResult = new RequestMatchResult();
requestMatchResult.AddScore(typeof(WebSocketMessageDirection), MatchScores.Perfect, null);
// Create log entry
var logEntry = new LogEntry
{
Guid = Guid.NewGuid(),
RequestMessage = requestMessage,
ResponseMessage = responseMessage,
MappingGuid = Mapping.Guid,
MappingTitle = Mapping.Title,
RequestMatchResult = requestMatchResult
};
// 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();
}
/// <inheritdoc />
public async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription)
{
await WebSocket.CloseAsync(closeStatus, statusDescription, CancellationToken.None);
LogWebSocketMessage(WebSocketMessageDirection.Send, WebSocketMessageType.Close, $"CloseStatus: {closeStatus}, Description: {statusDescription}", null);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -108,7 +253,7 @@ public class WireMockWebSocketContext : IWebSocketContext
} }
// Use the same logic as WireMockMiddleware // Use the same logic as WireMockMiddleware
if (_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenarioState)) if (Options.Scenarios.TryGetValue(Mapping.Scenario, out var scenarioState))
{ {
// Directly set the next state (bypass counter logic for manual WebSocket state changes) // Directly set the next state (bypass counter logic for manual WebSocket state changes)
scenarioState.NextState = nextState; scenarioState.NextState = nextState;
@@ -121,7 +266,7 @@ public class WireMockWebSocketContext : IWebSocketContext
else else
{ {
// Create new scenario state if it doesn't exist // Create new scenario state if it doesn't exist
_options.Scenarios.TryAdd(Mapping.Scenario, new ScenarioState Options.Scenarios.TryAdd(Mapping.Scenario, new ScenarioState
{ {
Name = Mapping.Scenario, Name = Mapping.Scenario,
NextState = nextState, NextState = nextState,
@@ -144,7 +289,7 @@ public class WireMockWebSocketContext : IWebSocketContext
} }
// Ensure scenario exists // Ensure scenario exists
if (!_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenario)) if (!Options.Scenarios.TryGetValue(Mapping.Scenario, out var scenario))
{ {
return; return;
} }

View File

@@ -67,7 +67,11 @@ public sealed class TestOutputHelperWireMockLogger : IWireMockLogger
/// <inheritdoc /> /// <inheritdoc />
public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest) public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest)
{ {
var message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented); var message = JsonConvert.SerializeObject(logEntryModel, new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
});
_testOutputHelper.WriteLine(Format("DebugRequestResponse", "Admin[{0}] {1}", isAdminRequest, message)); _testOutputHelper.WriteLine(Format("DebugRequestResponse", "Admin[{0}] {1}", isAdminRequest, message));
} }

View File

@@ -67,7 +67,11 @@ public sealed class TestOutputHelperWireMockLogger : IWireMockLogger
/// <inheritdoc /> /// <inheritdoc />
public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest) public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest)
{ {
var message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented); var message = JsonConvert.SerializeObject(logEntryModel, new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
});
_testOutputHelper.WriteLine(Format("DebugRequestResponse", "Admin[{0}] {1}", isAdminRequest, message)); _testOutputHelper.WriteLine(Format("DebugRequestResponse", "Admin[{0}] {1}", isAdminRequest, message));
} }