mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-03-28 20:32:13 +01:00
log
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user