mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-02-18 00:07:41 +01:00
log
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
|
||||
namespace WireMock.Admin.Requests;
|
||||
|
||||
/// <summary>
|
||||
@@ -17,12 +15,12 @@ public class LogEntryModel
|
||||
/// <summary>
|
||||
/// The request.
|
||||
/// </summary>
|
||||
public required LogRequestModel Request { get; init; }
|
||||
public LogRequestModel? Request { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The response.
|
||||
/// </summary>
|
||||
public required LogResponseModel Response { get; init; }
|
||||
public LogResponseModel? Response { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The mapping unique identifier.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Types;
|
||||
|
||||
|
||||
@@ -64,12 +64,12 @@ public class LogResponseModel
|
||||
/// <summary>
|
||||
/// The detected body type (detection based on body content).
|
||||
/// </summary>
|
||||
public BodyType? DetectedBodyType { get; set; }
|
||||
public string? DetectedBodyType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The detected body type (detection based on Content-Type).
|
||||
/// </summary>
|
||||
public BodyType? DetectedBodyTypeFromContentType { get; set; }
|
||||
public string? DetectedBodyTypeFromContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The FaultType.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Generic;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
@@ -52,6 +51,11 @@ public interface IResponseMessage
|
||||
/// </summary>
|
||||
object? StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DateTime.
|
||||
/// </summary>
|
||||
DateTime DateTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the header.
|
||||
/// </summary>
|
||||
|
||||
@@ -38,20 +38,20 @@ public interface ILogEntry
|
||||
/// <summary>
|
||||
/// Gets the partial match result.
|
||||
/// </summary>
|
||||
IRequestMatchResult PartialMatchResult { get; }
|
||||
IRequestMatchResult? PartialMatchResult { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request match result.
|
||||
/// </summary>
|
||||
IRequestMatchResult RequestMatchResult { get; }
|
||||
IRequestMatchResult? RequestMatchResult { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request message.
|
||||
/// </summary>
|
||||
IRequestMessage RequestMessage { get; }
|
||||
IRequestMessage? RequestMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response message.
|
||||
/// </summary>
|
||||
IResponseMessage ResponseMessage { get; }
|
||||
IResponseMessage? ResponseMessage { get; }
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using WireMock.Matchers.Request;
|
||||
|
||||
namespace WireMock.Logging;
|
||||
@@ -14,13 +13,13 @@ public class LogEntry : ILogEntry
|
||||
public Guid Guid { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ILogEntry.RequestMessage" />
|
||||
public IRequestMessage RequestMessage { get; set; } = null!;
|
||||
public IRequestMessage? RequestMessage { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ILogEntry.ResponseMessage" />
|
||||
public IResponseMessage ResponseMessage { get; set; } = null!;
|
||||
public IResponseMessage? ResponseMessage { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ILogEntry.RequestMatchResult" />
|
||||
public IRequestMatchResult RequestMatchResult { get; set; } = null!;
|
||||
public IRequestMatchResult? RequestMatchResult { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ILogEntry.MappingGuid" />
|
||||
public Guid? MappingGuid { get; set; }
|
||||
@@ -35,5 +34,5 @@ public class LogEntry : ILogEntry
|
||||
public string? PartialMappingTitle { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ILogEntry.PartialMatchResult" />
|
||||
public IRequestMatchResult PartialMatchResult { get; set; } = null!;
|
||||
public IRequestMatchResult? PartialMatchResult { get; set; }
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net.WebSockets;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Settings;
|
||||
using WireMock.WebSockets;
|
||||
|
||||
namespace WireMock.Owin.ActivityTracing;
|
||||
|
||||
@@ -195,4 +197,59 @@ internal static class WireMockActivitySource
|
||||
activity.SetTag("exception.message", exception.Message);
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,10 @@ internal static class WireMockSemanticConventions
|
||||
public const string RequestGuid = "wiremock.request.guid";
|
||||
public const string RequestBody = "wiremock.request.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";
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Diagnostics;
|
||||
using WireMock.Logging;
|
||||
|
||||
namespace WireMock.Owin;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -42,7 +42,8 @@ internal class WireMockMiddleware(
|
||||
private async Task InvokeInternalAsync(HttpContext ctx)
|
||||
{
|
||||
// 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);
|
||||
|
||||
@@ -158,7 +159,7 @@ internal class WireMockMiddleware(
|
||||
}
|
||||
finally
|
||||
{
|
||||
logger.Log(logRequest, request, response, result.Match, result.Partial, activity);
|
||||
logger.LogRequestAndResponse(logRequest, request, response, result.Match, result.Partial, activity);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -2,43 +2,46 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Matchers.Request;
|
||||
using WireMock.Owin.ActivityTracing;
|
||||
using WireMock.Serialization;
|
||||
using WireMock.Util;
|
||||
|
||||
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(),
|
||||
RequestMessage = request,
|
||||
ResponseMessage = response ?? new ResponseMessage(),
|
||||
ResponseMessage = response,
|
||||
|
||||
MappingGuid = match?.Mapping?.Guid,
|
||||
MappingTitle = match?.Mapping?.Title,
|
||||
RequestMatchResult = match?.RequestMatchResult ?? new RequestMatchResult(),
|
||||
RequestMatchResult = match?.RequestMatchResult,
|
||||
|
||||
PartialMappingGuid = partialMatch?.Mapping?.Guid,
|
||||
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();
|
||||
|
||||
LogRequest(log, logRequest);
|
||||
LogLogEntry(logEntry, logRequest);
|
||||
|
||||
try
|
||||
{
|
||||
if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
|
||||
{
|
||||
var filename = $"{log.Guid}.LogEntry.json";
|
||||
_options.FileSystemHandler?.WriteUnmatchedRequest(filename, JsonUtils.Serialize(log));
|
||||
var filename = $"{logEntry.Guid}.LogEntry.json";
|
||||
_options.FileSystemHandler?.WriteUnmatchedRequest(filename, JsonUtils.Serialize(logEntry));
|
||||
}
|
||||
}
|
||||
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 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)
|
||||
if (entry.RequestMessage != null)
|
||||
{
|
||||
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.
|
||||
if (_options.MaxRequestLogCount is > 0)
|
||||
{
|
||||
var logEntries = _options.LogEntries.ToList();
|
||||
foreach (var logEntry in logEntries.OrderBy(le => le.RequestMessage.DateTime).Take(logEntries.Count - _options.MaxRequestLogCount.Value))
|
||||
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))
|
||||
{
|
||||
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.
|
||||
if (_options.RequestLogExpirationDuration is > 0)
|
||||
{
|
||||
var logEntries = _options.LogEntries.Where(le => le.RequestMessage != null).ToList();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
// 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.
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Stef.Validation;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
using Stef.Validation;
|
||||
using WireMock.WebSockets;
|
||||
|
||||
namespace WireMock;
|
||||
|
||||
@@ -41,6 +38,9 @@ public class ResponseMessage : IResponseMessage
|
||||
/// <inheritdoc cref="IResponseMessage.FaultPercentage" />
|
||||
public double? FaultPercentage { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime DateTime { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using WireMock.Constants;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Matchers.Request;
|
||||
using WireMock.Models;
|
||||
using WireMock.Owin;
|
||||
using WireMock.Owin.ActivityTracing;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
using WireMock.WebSockets;
|
||||
|
||||
namespace WireMock.ResponseProviders;
|
||||
@@ -42,10 +51,17 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
|
||||
#endif
|
||||
|
||||
// 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)
|
||||
{
|
||||
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
|
||||
@@ -60,7 +76,9 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
|
||||
requestMessage,
|
||||
mapping,
|
||||
registry,
|
||||
builder
|
||||
builder,
|
||||
options,
|
||||
logger
|
||||
);
|
||||
|
||||
// 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);
|
||||
using var cts = new CancellationTokenSource(timeout);
|
||||
|
||||
var shouldTrace = context.Options?.ActivityTracingOptions is not null;
|
||||
|
||||
try
|
||||
{
|
||||
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await context.WebSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cts.Token
|
||||
).ConfigureAwait(false);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
Activity? receiveActivity = null;
|
||||
if (shouldTrace)
|
||||
{
|
||||
await context.CloseAsync(
|
||||
WebSocketCloseStatus.NormalClosure,
|
||||
"Closed by client"
|
||||
).ConfigureAwait(false);
|
||||
break;
|
||||
receiveActivity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Receive, context.Mapping.Guid);
|
||||
}
|
||||
|
||||
// Echo back
|
||||
await context.WebSocket.SendAsync(
|
||||
new ArraySegment<byte>(buffer, 0, result.Count),
|
||||
result.MessageType,
|
||||
result.EndOfMessage,
|
||||
cts.Token
|
||||
).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var result = await context.WebSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cts.Token
|
||||
).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)
|
||||
@@ -169,28 +243,79 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
|
||||
var timeout = context.Builder.CloseTimeout ?? TimeSpan.FromMinutes(WebSocketConstants.DefaultCloseTimeoutMinutes);
|
||||
using var cts = new CancellationTokenSource(timeout);
|
||||
|
||||
var shouldTrace = context.Options?.ActivityTracingOptions is not null;
|
||||
|
||||
try
|
||||
{
|
||||
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await context.WebSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cts.Token
|
||||
).ConfigureAwait(false);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
Activity? receiveActivity = null;
|
||||
if (shouldTrace)
|
||||
{
|
||||
await context.CloseAsync(
|
||||
WebSocketCloseStatus.NormalClosure,
|
||||
"Closed by client"
|
||||
).ConfigureAwait(false);
|
||||
break;
|
||||
receiveActivity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Receive, context.Mapping.Guid);
|
||||
}
|
||||
|
||||
var message = CreateWebSocketMessage(result, buffer);
|
||||
try
|
||||
{
|
||||
var result = await context.WebSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cts.Token
|
||||
).ConfigureAwait(false);
|
||||
|
||||
// Call custom handler
|
||||
await handler(message, context).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;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -210,8 +335,8 @@ internal class WebSocketResponseProvider(WebSocketBuilder builder) : IResponsePr
|
||||
await clientWebSocket.ConnectAsync(targetUri, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Bidirectional proxy
|
||||
var clientToServer = ForwardMessagesAsync(context.WebSocket, clientWebSocket);
|
||||
var serverToClient = ForwardMessagesAsync(clientWebSocket, context.WebSocket);
|
||||
var clientToServer = ForwardMessagesAsync(context, clientWebSocket, WebSocketMessageDirection.Receive);
|
||||
var serverToClient = ForwardMessagesAsync(context, clientWebSocket, WebSocketMessageDirection.Send);
|
||||
|
||||
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];
|
||||
|
||||
// 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)
|
||||
{
|
||||
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
Activity? activity = null;
|
||||
if (shouldTrace)
|
||||
{
|
||||
await destination.CloseAsync(
|
||||
result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
||||
result.CloseStatusDescription,
|
||||
CancellationToken.None
|
||||
);
|
||||
break;
|
||||
activity = WireMockActivitySource.StartWebSocketMessageActivity(direction, context.Mapping.Guid);
|
||||
}
|
||||
|
||||
await destination.SendAsync(
|
||||
new ArraySegment<byte>(buffer, 0, result.Count),
|
||||
result.MessageType,
|
||||
result.EndOfMessage,
|
||||
CancellationToken.None
|
||||
);
|
||||
try
|
||||
{
|
||||
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), 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);
|
||||
using var cts = new CancellationTokenSource(timeout);
|
||||
|
||||
var shouldTrace = context.Options?.ActivityTracingOptions is not null;
|
||||
|
||||
try
|
||||
{
|
||||
while (context.WebSocket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await context.WebSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cts.Token
|
||||
);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
Activity? receiveActivity = null;
|
||||
if (shouldTrace)
|
||||
{
|
||||
await context.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client");
|
||||
break;
|
||||
receiveActivity = WireMockActivitySource.StartWebSocketMessageActivity(WebSocketMessageDirection.Receive, context.Mapping.Guid);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var message = new WebSocketMessage
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Stef.Validation;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Admin.Requests;
|
||||
using WireMock.Logging;
|
||||
@@ -11,95 +10,96 @@ using WireMock.Types;
|
||||
|
||||
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)
|
||||
{
|
||||
var logRequestModel = new LogRequestModel
|
||||
LogRequestModel? logRequestModel = null;
|
||||
if (logEntry.RequestMessage != null)
|
||||
{
|
||||
DateTime = logEntry.RequestMessage.DateTime,
|
||||
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)
|
||||
logRequestModel = new LogRequestModel
|
||||
{
|
||||
case BodyType.String:
|
||||
case BodyType.FormUrlEncoded:
|
||||
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString;
|
||||
break;
|
||||
DateTime = logEntry.RequestMessage.DateTime,
|
||||
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
|
||||
};
|
||||
|
||||
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;
|
||||
if (logEntry.RequestMessage.BodyData != null)
|
||||
{
|
||||
logRequestModel.DetectedBodyType = logEntry.RequestMessage.BodyData.DetectedBodyType?.ToString();
|
||||
logRequestModel.DetectedBodyTypeFromContentType = logEntry.RequestMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
|
||||
|
||||
case BodyType.Bytes:
|
||||
logRequestModel.BodyAsBytes = logEntry.RequestMessage.BodyData.BodyAsBytes;
|
||||
break;
|
||||
switch (logEntry.RequestMessage.BodyData.DetectedBodyType)
|
||||
{
|
||||
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
|
||||
? new EncodingModel
|
||||
{
|
||||
EncodingName = logEntry.RequestMessage.BodyData.Encoding.EncodingName,
|
||||
CodePage = logEntry.RequestMessage.BodyData.Encoding.CodePage,
|
||||
WebName = logEntry.RequestMessage.BodyData.Encoding.WebName
|
||||
}
|
||||
: null;
|
||||
}
|
||||
if (logEntry.ResponseMessage.BodyData != null)
|
||||
{
|
||||
logResponseModel.BodyOriginal = logEntry.ResponseMessage.BodyOriginal;
|
||||
logResponseModel.BodyDestination = logEntry.ResponseMessage.BodyDestination;
|
||||
|
||||
var logResponseModel = new LogResponseModel
|
||||
{
|
||||
StatusCode = logEntry.ResponseMessage.StatusCode,
|
||||
Headers = logEntry.ResponseMessage.Headers
|
||||
};
|
||||
logResponseModel.DetectedBodyType = logEntry.ResponseMessage.BodyData.DetectedBodyType?.ToString();
|
||||
logResponseModel.DetectedBodyTypeFromContentType = logEntry.ResponseMessage.BodyData.DetectedBodyTypeFromContentType?.ToString();
|
||||
|
||||
if (logEntry.ResponseMessage.FaultType != FaultType.NONE)
|
||||
{
|
||||
logResponseModel.FaultType = logEntry.ResponseMessage.FaultType.ToString();
|
||||
logResponseModel.FaultPercentage = logEntry.ResponseMessage.FaultPercentage;
|
||||
}
|
||||
MapBody(logEntry, logResponseModel);
|
||||
|
||||
if (logEntry.ResponseMessage.BodyData != null)
|
||||
{
|
||||
logResponseModel.BodyOriginal = logEntry.ResponseMessage.BodyOriginal;
|
||||
logResponseModel.BodyDestination = logEntry.ResponseMessage.BodyDestination;
|
||||
|
||||
logResponseModel.DetectedBodyType = logEntry.ResponseMessage.BodyData.DetectedBodyType;
|
||||
logResponseModel.DetectedBodyTypeFromContentType = logEntry.ResponseMessage.BodyData.DetectedBodyTypeFromContentType;
|
||||
|
||||
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;
|
||||
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
|
||||
@@ -120,11 +120,11 @@ internal class LogEntryMapper
|
||||
|
||||
private void MapBody(ILogEntry logEntry, LogResponseModel logResponseModel)
|
||||
{
|
||||
switch (logEntry.ResponseMessage.BodyData!.DetectedBodyType)
|
||||
switch (logEntry.ResponseMessage?.BodyData?.DetectedBodyType)
|
||||
{
|
||||
case BodyType.String:
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using WireMock.WebSockets;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Stef.Validation;
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Matchers.Request;
|
||||
using WireMock.Models;
|
||||
using WireMock.Owin;
|
||||
using WireMock.Owin.ActivityTracing;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.WebSockets;
|
||||
|
||||
@@ -14,8 +23,6 @@ namespace WireMock.WebSockets;
|
||||
/// </summary>
|
||||
public class WireMockWebSocketContext : IWebSocketContext
|
||||
{
|
||||
private readonly IWireMockMiddlewareOptions _options;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ConnectionId { get; } = Guid.NewGuid();
|
||||
|
||||
@@ -35,6 +42,10 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
|
||||
internal WebSocketBuilder Builder { get; }
|
||||
|
||||
internal IWireMockMiddlewareOptions Options { get; }
|
||||
|
||||
internal IWireMockMiddlewareLogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new WebSocketContext
|
||||
/// </summary>
|
||||
@@ -44,7 +55,9 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
IRequestMessage requestMessage,
|
||||
IMapping mapping,
|
||||
WebSocketConnectionRegistry? registry,
|
||||
WebSocketBuilder builder)
|
||||
WebSocketBuilder builder,
|
||||
IWireMockMiddlewareOptions options,
|
||||
IWireMockMiddlewareLogger logger)
|
||||
{
|
||||
HttpContext = Guard.NotNull(httpContext);
|
||||
WebSocket = Guard.NotNull(webSocket);
|
||||
@@ -52,26 +65,19 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
Mapping = Guard.NotNull(mapping);
|
||||
Registry = registry;
|
||||
Builder = Guard.NotNull(builder);
|
||||
|
||||
// Get options from HttpContext
|
||||
if (httpContext.Items.TryGetValue<IWireMockMiddlewareOptions>(nameof(WireMockMiddlewareOptions), out var options))
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("WireMockMiddlewareOptions not found in HttpContext.Items");
|
||||
}
|
||||
Options = Guard.NotNull(options);
|
||||
Logger = Guard.NotNull(logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(string text, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
return WebSocket.SendAsync(
|
||||
return SendAsyncInternal(
|
||||
new ArraySegment<byte>(bytes),
|
||||
WebSocketMessageType.Text,
|
||||
true,
|
||||
text,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
@@ -79,18 +85,157 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(byte[] bytes, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return WebSocket.SendAsync(
|
||||
return SendAsyncInternal(
|
||||
new ArraySegment<byte>(bytes),
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
bytes,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription)
|
||||
private async Task SendAsyncInternal(
|
||||
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 />
|
||||
@@ -108,7 +253,7 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
}
|
||||
|
||||
// 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)
|
||||
scenarioState.NextState = nextState;
|
||||
@@ -121,7 +266,7 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
else
|
||||
{
|
||||
// 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,
|
||||
NextState = nextState,
|
||||
@@ -144,7 +289,7 @@ public class WireMockWebSocketContext : IWebSocketContext
|
||||
}
|
||||
|
||||
// Ensure scenario exists
|
||||
if (!_options.Scenarios.TryGetValue(Mapping.Scenario, out var scenario))
|
||||
if (!Options.Scenarios.TryGetValue(Mapping.Scenario, out var scenario))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,11 @@ public sealed class TestOutputHelperWireMockLogger : IWireMockLogger
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,11 @@ public sealed class TestOutputHelperWireMockLogger : IWireMockLogger
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user