Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions DevProxy.Abstractions/Models/MockStdioResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ public object Clone()
public class MockStdioRequest
{
/// <summary>
/// A fragment of the stdin body to match.
/// If null or empty, the mock matches any stdin (or is applied immediately on startup).
/// A fragment of the stdin body to match (case-insensitive contains).
/// Prefer using either <see cref="BodyFragment"/> or <see cref="BodyRegex"/>, not both.
/// If both are specified, <see cref="BodyRegex"/> takes precedence and <see cref="BodyFragment"/> is ignored.
/// If neither is specified, the mock matches any stdin (or is applied immediately on startup).
/// </summary>
public string? BodyFragment { get; set; }

/// <summary>
/// A regular expression pattern to match against the stdin body (case-insensitive).
/// Prefer using either <see cref="BodyRegex"/> or <see cref="BodyFragment"/>, not both.
/// If both are specified, <see cref="BodyRegex"/> takes precedence and <see cref="BodyFragment"/> is ignored.
/// If neither is specified, the mock matches any stdin (or is applied immediately on startup).
/// </summary>
public string? BodyRegex { get; set; }

/// <summary>
/// The Nth occurrence to match. If null, matches every occurrence.
/// </summary>
Expand Down
50 changes: 42 additions & 8 deletions DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ private void ApplyStartupMocks(StdioRequestArgs e)

// Find mocks without stdin patterns (startup mocks)
var startupMocks = Configuration.Mocks
.Where(m => string.IsNullOrEmpty(m.Request?.BodyFragment))
.Where(m => string.IsNullOrEmpty(m.Request?.BodyFragment) &&
string.IsNullOrEmpty(m.Request?.BodyRegex))
.ToList();

foreach (var mock in startupMocks)
Expand Down Expand Up @@ -249,16 +250,42 @@ Configuration.Mocks is null ||
return false;
}

if (string.IsNullOrEmpty(mockResponse.Request.BodyFragment))
var hasBodyFragment = !string.IsNullOrEmpty(mockResponse.Request.BodyFragment);
var hasBodyRegex = !string.IsNullOrEmpty(mockResponse.Request.BodyRegex);

if (!hasBodyFragment && !hasBodyRegex)
{
// No body fragment means startup mock, handled separately
// No body fragment or regex means startup mock, handled separately
return false;
}

// Check if stdin contains the body fragment
if (!stdinBody.Contains(mockResponse.Request.BodyFragment, StringComparison.OrdinalIgnoreCase))
// Use bodyRegex if specified, otherwise use bodyFragment (either/or)
if (hasBodyRegex)
{
return false;
try
{
if (!Regex.IsMatch(stdinBody, mockResponse.Request.BodyRegex!, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5)))
{
return false;
}
}
catch (RegexMatchTimeoutException ex)
{
Logger.LogError(ex, "Regex match timed out for body regex pattern '{Pattern}'. Treating as non-matching mock.", mockResponse.Request.BodyRegex);
return false;
}
catch (ArgumentException ex)
{
Logger.LogError(ex, "Invalid body regex pattern '{Pattern}'. Treating as non-matching mock.", mockResponse.Request.BodyRegex);
return false;
}
}
else if (hasBodyFragment)
{
if (!stdinBody.Contains(mockResponse.Request.BodyFragment!, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}

// Check Nth condition
Expand All @@ -267,7 +294,7 @@ Configuration.Mocks is null ||

if (mockResponse?.Request is not null)
{
var mockKey = mockResponse.Request.BodyFragment ?? "default";
var mockKey = GetMockKey(mockResponse);
_ = _appliedMocks.AddOrUpdate(mockKey, 1, (_, value) => ++value);
}

Expand All @@ -282,13 +309,20 @@ private bool IsNthRequest(MockStdioResponse mockResponse)
return true;
}

var mockKey = mockResponse.Request.BodyFragment ?? "default";
var mockKey = GetMockKey(mockResponse);
_ = _appliedMocks.TryGetValue(mockKey, out var nth);
nth++;

return mockResponse.Request.Nth == nth;
}

private static string GetMockKey(MockStdioResponse mockResponse)
{
return mockResponse.Request?.BodyRegex
?? mockResponse.Request?.BodyFragment
?? "default";
}

private void ProcessMockResponse(StdioRequestArgs e, MockStdioResponse matchingResponse)
{
// Replace placeholders in the response with values from stdin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
"properties": {
"bodyFragment": {
"type": "string",
"description": "(Optional) A fragment of the stdin body to match. If null or empty, the mock matches any stdin or is applied immediately on startup."
"description": "(Optional) A fragment of the stdin body to match (case-insensitive contains). You can specify bodyFragment, bodyRegex, or both; if both are specified, bodyRegex takes precedence. If neither is specified, the mock matches any stdin or is applied immediately on startup."
},
"bodyRegex": {
"type": "string",
"description": "(Optional) A regular expression pattern to match against the stdin body (case-insensitive). You can specify bodyRegex, bodyFragment, or both; if both are specified, bodyRegex takes precedence. If neither is specified, the mock matches any stdin or is applied immediately on startup."
},
"nth": {
"type": "integer",
Expand Down
Loading