diff --git a/DevProxy.Abstractions/Models/MockStdioResponse.cs b/DevProxy.Abstractions/Models/MockStdioResponse.cs index 4a39a24e..47e01132 100644 --- a/DevProxy.Abstractions/Models/MockStdioResponse.cs +++ b/DevProxy.Abstractions/Models/MockStdioResponse.cs @@ -34,11 +34,21 @@ public object Clone() public class MockStdioRequest { /// - /// 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 or , not both. + /// If both are specified, takes precedence and is ignored. + /// If neither is specified, the mock matches any stdin (or is applied immediately on startup). /// public string? BodyFragment { get; set; } + /// + /// A regular expression pattern to match against the stdin body (case-insensitive). + /// Prefer using either or , not both. + /// If both are specified, takes precedence and is ignored. + /// If neither is specified, the mock matches any stdin (or is applied immediately on startup). + /// + public string? BodyRegex { get; set; } + /// /// The Nth occurrence to match. If null, matches every occurrence. /// diff --git a/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs b/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs index 09a38322..885b2207 100644 --- a/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs +++ b/DevProxy.Plugins/Mocking/MockStdioResponsePlugin.cs @@ -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) @@ -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 @@ -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); } @@ -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 diff --git a/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json b/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json index 23582644..3579f028 100644 --- a/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json +++ b/schemas/v2.2.0/mockstdioresponseplugin.mocksfile.schema.json @@ -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",