mockserver-client 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +102 -1
- data/lib/mockserver/a2a.rb +4 -3
- data/lib/mockserver/client.rb +135 -16
- data/lib/mockserver/forward_chain_expectation.rb +8 -0
- data/lib/mockserver/mcp.rb +20 -3
- data/lib/mockserver/models.rb +543 -43
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver/websocket_client.rb +5 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02d995291d2bc1c0c4b875711f568d604c4e6af009952512109b44ff49fa2fed
|
|
4
|
+
data.tar.gz: 455d49792d3fec33d41dd66eade0ca64e5ee4049306566c411da1008a431b30a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71d509505576e64dd73a68b79352bbdd0065519b5775767025340798032832bccffa5806b5bac0cda568286b0202baf7af48f84155a095df301df3c60e6be500
|
|
7
|
+
data.tar.gz: 5d6c9b72e335529472b51013bfbaad1d521be2784267d95a48284d2a777577a19ccd87b5f68eedae3bab6195e774d80880e4712f4f289993384d84309433cd92
|
data/README.md
CHANGED
|
@@ -56,6 +56,15 @@ end
|
|
|
56
56
|
# Client is automatically closed when the block exits
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## Control-Plane Basic Authentication
|
|
60
|
+
|
|
61
|
+
When MockServer accepts control-plane Basic authentication, pass `control_plane_basic_auth: [username, password]` with `secure: true` (MockServer refuses Basic credentials without TLS). They are sent on every control-plane request and on the callback and breakpoint WebSockets.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
client = MockServer::Client.new('localhost', 1080, secure: true,
|
|
65
|
+
control_plane_basic_auth: ['admin', ENV.fetch('MOCKSERVER_PASSWORD')])
|
|
66
|
+
```
|
|
67
|
+
|
|
59
68
|
## WebSocket Callbacks
|
|
60
69
|
|
|
61
70
|
```ruby
|
|
@@ -99,6 +108,7 @@ All 27 domain model classes are available under the `MockServer` module:
|
|
|
99
108
|
- `Ports`
|
|
100
109
|
- `RequestDefinition` (alias for `HttpRequest`)
|
|
101
110
|
- `Jwt` (JWT / bearer-token request matcher)
|
|
111
|
+
- `DateTimeMatcher` (date-time matcher for header, query, path-parameter and cookie values)
|
|
102
112
|
|
|
103
113
|
Every model can also be built from the expectation JSON with `from_hash`. Keys
|
|
104
114
|
may be strings (`'httpRequest' => {...}`, as `JSON.parse` returns them) or
|
|
@@ -129,6 +139,41 @@ MockServer::HttpRequest.new(
|
|
|
129
139
|
)
|
|
130
140
|
```
|
|
131
141
|
|
|
142
|
+
A header, query parameter, path parameter or cookie value can also be a
|
|
143
|
+
date-time matcher. It compares the request's value as a date-time with
|
|
144
|
+
`before`, `after` or `equal_to_date_time`, where the expected value is `now`,
|
|
145
|
+
`now` with an offset such as `now -1 hours`, or a date-time such as
|
|
146
|
+
`2026-10-21T00:00:00Z`. A plain string such as `'before:2026-01-01'` is still
|
|
147
|
+
matched literally.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
MockServer::HttpRequest.new(path: '/orders')
|
|
151
|
+
.with_header('X-Issued-At', MockServer::DateTimeMatcher.after('now -1 hours'))
|
|
152
|
+
.with_header('If-Modified-Since',
|
|
153
|
+
MockServer::DateTimeMatcher.before('2026-10-21T00:00:00Z').with_actual_format('rfc1123'))
|
|
154
|
+
.with_cookie('seen', MockServer::DateTimeMatcher.equal_to_date_time('now')
|
|
155
|
+
.with_truncate_expected('startOfDay')
|
|
156
|
+
.with_truncate_actual('startOfDay')
|
|
157
|
+
.with_zone('Europe/London'))
|
|
158
|
+
# headers => [{"name"=>"X-Issued-At", "values"=>[{"after"=>"now -1 hours"}]}, ...]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each `with_*` option (`with_actual_format`, `with_truncate_expected`,
|
|
162
|
+
`with_truncate_actual`, `with_apply_truncation_last`, `with_zone`, `with_not`)
|
|
163
|
+
returns a new matcher. `from_hash` reads an object holding `before`, `after` or
|
|
164
|
+
`equalToDateTime` as a `DateTimeMatcher`.
|
|
165
|
+
|
|
166
|
+
A JSONPath or XPath body matcher takes a `date_time:` too: the body matches when
|
|
167
|
+
a value the JSONPath selects, or the text of a node the XPath selects, is a
|
|
168
|
+
matching date-time.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
MockServer::HttpRequest.new(path: '/orders',
|
|
172
|
+
body: MockServer::Body.json_path('$.order.created',
|
|
173
|
+
date_time: MockServer::DateTimeMatcher.after('now -1 days')))
|
|
174
|
+
# body => {"type"=>"JSON_PATH", "jsonPath"=>"$.order.created", "dateTime"=>{"after"=>"now -1 days"}}
|
|
175
|
+
```
|
|
176
|
+
|
|
132
177
|
An `httpRequest` holding `specUrlOrPayload` is read as an `OpenAPIDefinition`
|
|
133
178
|
matcher, and `HttpOverrideForwardedRequest` carries `request_override`,
|
|
134
179
|
`response_override` and `response_template`. These need a client released
|
|
@@ -185,6 +230,28 @@ body = MockServer::Body.all_of(
|
|
|
185
230
|
request = MockServer::HttpRequest.new(method: 'POST', path: '/api').with_body(body)
|
|
186
231
|
```
|
|
187
232
|
|
|
233
|
+
## Client IP matcher
|
|
234
|
+
|
|
235
|
+
Match requests by the client IP address or CIDR range, IPv4 or IPv6. The client address is the TCP peer of the connection, not `X-Forwarded-For`; prefix with `!` to match every other address.
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
MockServer::HttpRequest.new(path: '/internal').with_client_ip('10.0.0.0/8')
|
|
239
|
+
# or MockServer::HttpRequest.new(path: '/internal', client_ip: '!10.0.0.0/8')
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Any-of (logical OR) matcher
|
|
243
|
+
|
|
244
|
+
`MockServer::AnyOfRequestDefinition` matches a request when any one of its request matchers does, tried in order. Entries may be `HttpRequest`, `OpenAPIDefinition`, `ConditionalRequestDefinition` or a nested `AnyOfRequestDefinition`; `not_any_of: true` matches requests that match none of them. `ExplainedExpectation#any_of` says which entry came closest when none matches.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
client.when(
|
|
248
|
+
MockServer::AnyOfRequestDefinition.new(
|
|
249
|
+
any_of: [MockServer::HttpRequest.new(method: 'GET', path: '/pets'),
|
|
250
|
+
MockServer::HttpRequest.new(method: 'GET', path: '/animals')]
|
|
251
|
+
)
|
|
252
|
+
).respond(MockServer::HttpResponse.new(body: 'pets or animals'))
|
|
253
|
+
```
|
|
254
|
+
|
|
188
255
|
## LLM Mocking
|
|
189
256
|
|
|
190
257
|
Fluent builders under `MockServer::LLM` mock LLM provider endpoints. They produce
|
|
@@ -300,6 +367,40 @@ JSON-RPC task response to the caller *and* POSTs the completed task to the
|
|
|
300
367
|
webhook URL. As with MCP mocking, `build` returns the raw expectation Hashes if
|
|
301
368
|
you prefer to register them yourself.
|
|
302
369
|
|
|
370
|
+
## Expectation Metadata
|
|
371
|
+
|
|
372
|
+
Label expectations with `metadata` (String keys, any JSON value), then retrieve or clear them by
|
|
373
|
+
metadata, find the ones that never matched a request, and page large lists:
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
client.when(MockServer::HttpRequest.new(path: '/checkout'))
|
|
377
|
+
.with_metadata('owner' => 'team-a', 'ticket' => 4211, 'tags' => ['smoke'])
|
|
378
|
+
.respond(MockServer::HttpResponse.new(status_code: 200))
|
|
379
|
+
|
|
380
|
+
client.retrieve_active_expectations_by_metadata('owner', 'team-a')
|
|
381
|
+
client.retrieve_active_expectations_by_metadata_key('ticket')
|
|
382
|
+
client.retrieve_unmatched_expectations
|
|
383
|
+
client.retrieve_active_expectations(
|
|
384
|
+
filter: MockServer::ExpectationFilter.new.with_metadata_json_path("$.tags[?(@ == 'smoke')]").with_limit(10)
|
|
385
|
+
)
|
|
386
|
+
client.retrieve_recorded_expectations(offset: 0, limit: 50)
|
|
387
|
+
# replay changing responses in order, keep only some request headers, move large bodies into files
|
|
388
|
+
client.retrieve_recorded_expectations(
|
|
389
|
+
options: MockServer::RecordedExpectationsOptions.new.with_repeats_as_scenarios
|
|
390
|
+
.with_capture_headers('X-Tenant')
|
|
391
|
+
.with_extract_bodies_larger_than(1024)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
client.clear_by_metadata('owner', 'team-a')
|
|
395
|
+
client.clear_by_metadata_key('ticket')
|
|
396
|
+
client.clear_unmatched_expectations
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
A String value is matched as text; any other value (`42`, `true`, an Array or Hash) is matched as
|
|
400
|
+
JSON, and `MockServer::ExpectationFilter::JSON_NULL` selects a stored JSON null. A blank key or
|
|
401
|
+
JSONPath, a `nil` value, negative paging, paging on clear, or a clear with no selection raises
|
|
402
|
+
`ArgumentError` before anything is sent, so a mistake can never clear every expectation.
|
|
403
|
+
|
|
303
404
|
## SRE Control Plane (SLO + Chaos)
|
|
304
405
|
|
|
305
406
|
Verify service-level objectives and drive scheduled chaos experiments against
|
|
@@ -417,7 +518,7 @@ launcher_path = MockServer::BinaryLauncher.ensure_launcher
|
|
|
417
518
|
### Specify a version
|
|
418
519
|
|
|
419
520
|
```ruby
|
|
420
|
-
handle = MockServer::BinaryLauncher.start(port: 1080, version: '9.
|
|
521
|
+
handle = MockServer::BinaryLauncher.start(port: 1080, version: '9.2.0')
|
|
421
522
|
```
|
|
422
523
|
|
|
423
524
|
### API reference
|
data/lib/mockserver/a2a.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'uri'
|
|
5
|
+
require_relative 'mcp'
|
|
5
6
|
|
|
6
7
|
module MockServer
|
|
7
8
|
# Fluent builder for mocking an A2A (Agent-to-Agent) agent.
|
|
@@ -45,11 +46,11 @@ module MockServer
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Escape Velocity metacharacters so literal +$+ / +#+ survive rendering.
|
|
49
|
+
# Shares the MCP builder's rules (backslash runs before +$+, +#+ or the end
|
|
50
|
+
# of the value are escaped too).
|
|
48
51
|
# @api private
|
|
49
52
|
def self.escape_velocity(value)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
value.to_s.gsub('$', '${esc.d}').gsub('#', '${esc.h}')
|
|
53
|
+
MCP.escape_velocity(value)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
# @api private
|
data/lib/mockserver/client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'net/http'
|
|
|
4
4
|
require 'uri'
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'openssl'
|
|
7
|
+
require 'base64'
|
|
7
8
|
|
|
8
9
|
module MockServer
|
|
9
10
|
# Synchronous MockServer client.
|
|
@@ -28,14 +29,28 @@ module MockServer
|
|
|
28
29
|
class Client
|
|
29
30
|
HTTP_TIMEOUT = 60 # seconds, matching Python client
|
|
30
31
|
|
|
32
|
+
# Default for clear's +filter:+ so an explicit nil filter is rejected
|
|
33
|
+
# instead of silently clearing everything.
|
|
34
|
+
NO_FILTER = Object.new.freeze
|
|
35
|
+
private_constant :NO_FILTER
|
|
36
|
+
|
|
31
37
|
# @param host [String]
|
|
32
38
|
# @param port [Integer]
|
|
33
39
|
# @param context_path [String]
|
|
34
40
|
# @param secure [Boolean]
|
|
35
41
|
# @param ca_cert_path [String, nil]
|
|
36
42
|
# @param tls_verify [Boolean]
|
|
43
|
+
# @param control_plane_basic_auth [Array(String, String), nil] control-plane HTTP Basic
|
|
44
|
+
# +[username, password]+, sent on every control-plane request and on the callback and
|
|
45
|
+
# breakpoint WebSockets; MockServer refuses Basic credentials without TLS, so use +secure: true+
|
|
37
46
|
def initialize(host, port, context_path: '', secure: false,
|
|
38
|
-
ca_cert_path: nil, tls_verify: true)
|
|
47
|
+
ca_cert_path: nil, tls_verify: true, control_plane_basic_auth: nil)
|
|
48
|
+
@control_plane_authorization = basic_authorization(control_plane_basic_auth)
|
|
49
|
+
if @control_plane_authorization && !secure
|
|
50
|
+
warn 'MockServer::Client: control-plane Basic credentials are configured without secure: true, so the password ' \
|
|
51
|
+
'can be read on the network, and MockServer refuses them unless controlPlaneBasicAuthenticationAllowInsecure ' \
|
|
52
|
+
'is set (tests only)'
|
|
53
|
+
end
|
|
39
54
|
@host = host
|
|
40
55
|
@port = port
|
|
41
56
|
@context_path = context_path
|
|
@@ -100,10 +115,19 @@ module MockServer
|
|
|
100
115
|
end
|
|
101
116
|
|
|
102
117
|
# Clear expectations and/or logs.
|
|
118
|
+
#
|
|
119
|
+
# With +filter:+ only the expectations matching both +request+ (when
|
|
120
|
+
# given) and the filter's metadata / unmatched selection are cleared; the
|
|
121
|
+
# event log is left untouched. A filter must select something and must not
|
|
122
|
+
# page, otherwise ArgumentError is raised and nothing is sent.
|
|
123
|
+
#
|
|
103
124
|
# @param request [HttpRequest, nil]
|
|
104
125
|
# @param type [String, nil] "EXPECTATIONS", "LOG", or "ALL"
|
|
126
|
+
# @param filter [ExpectationFilter] selection of expectations to clear
|
|
105
127
|
# @return [nil]
|
|
106
|
-
def clear(request = nil, type: nil)
|
|
128
|
+
def clear(request = nil, type: nil, filter: NO_FILTER)
|
|
129
|
+
return clear_with_filter(request, type, filter) unless filter.equal?(NO_FILTER)
|
|
130
|
+
|
|
107
131
|
query_params = {}
|
|
108
132
|
query_params['type'] = type if type
|
|
109
133
|
body = request ? JSON.generate(request.to_h) : ''
|
|
@@ -117,6 +141,25 @@ module MockServer
|
|
|
117
141
|
nil
|
|
118
142
|
end
|
|
119
143
|
|
|
144
|
+
# Clear the expectations whose metadata has +key+ with a value equal to
|
|
145
|
+
# +value+ (see {ExpectationFilter#with_metadata}).
|
|
146
|
+
# @return [nil]
|
|
147
|
+
def clear_by_metadata(key, value)
|
|
148
|
+
clear(filter: ExpectationFilter.new.with_metadata(key, value))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Clear the expectations whose metadata has +key+, with any value.
|
|
152
|
+
# @return [nil]
|
|
153
|
+
def clear_by_metadata_key(key)
|
|
154
|
+
clear(filter: ExpectationFilter.new.with_metadata_key(key))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Clear the expectations that have not matched any request.
|
|
158
|
+
# @return [nil]
|
|
159
|
+
def clear_unmatched_expectations
|
|
160
|
+
clear(filter: ExpectationFilter.new.with_unmatched(true))
|
|
161
|
+
end
|
|
162
|
+
|
|
120
163
|
# Clear by expectation ID.
|
|
121
164
|
# @param expectation_id [String]
|
|
122
165
|
# @param type [String, nil]
|
|
@@ -265,6 +308,9 @@ module MockServer
|
|
|
265
308
|
#
|
|
266
309
|
# +config_json+ is a ConfigurationDTO JSON document; only the fields present
|
|
267
310
|
# are applied (partial update). A nil value is sent as an empty body.
|
|
311
|
+
# Properties fixed at startup, such as forwardProxyBlockPrivateNetworks,
|
|
312
|
+
# forwardProxyAllowedHosts and forwardProxyDeniedHosts, are ignored and the
|
|
313
|
+
# server logs a warning.
|
|
268
314
|
#
|
|
269
315
|
# @param config_json [String, nil] the ConfigurationDTO JSON
|
|
270
316
|
# @return [String] the serialized updated configuration JSON
|
|
@@ -912,14 +958,15 @@ module MockServer
|
|
|
912
958
|
#
|
|
913
959
|
# @param experiment [Hash, #to_h] the experiment definition
|
|
914
960
|
# @return [Hash] the started status, e.g. { "status" => "started", "name" => ... }
|
|
915
|
-
# @raise [Error] if the experiment definition is invalid
|
|
916
|
-
# (HTTP
|
|
961
|
+
# @raise [Error] if the experiment definition is invalid (HTTP 400), if
|
|
962
|
+
# control-plane authorization rejects the request (HTTP 403), or on any other
|
|
963
|
+
# failure; there is no server setting that disables chaos experiments
|
|
917
964
|
def start_chaos_experiment(experiment)
|
|
918
965
|
payload = experiment.respond_to?(:to_h) ? experiment.to_h : experiment
|
|
919
966
|
body = JSON.generate(payload)
|
|
920
967
|
status, response_body = request('PUT', '/mockserver/chaosExperiment', body)
|
|
921
968
|
if status == 403
|
|
922
|
-
raise Error,
|
|
969
|
+
raise Error, "Chaos experiment rejected (status=403): control-plane authorization denied: #{response_body}"
|
|
923
970
|
end
|
|
924
971
|
if status >= 400
|
|
925
972
|
raise Error, "Failed to start chaos experiment (status=#{status}): #{response_body}"
|
|
@@ -1173,13 +1220,17 @@ module MockServer
|
|
|
1173
1220
|
|
|
1174
1221
|
# Retrieve active expectations.
|
|
1175
1222
|
# @param request [HttpRequest, nil]
|
|
1223
|
+
# @param filter [ExpectationFilter, nil] metadata / unmatched selection and paging
|
|
1176
1224
|
# @return [Array<Expectation>]
|
|
1177
|
-
def retrieve_active_expectations(request: nil)
|
|
1225
|
+
def retrieve_active_expectations(request: nil, filter: nil)
|
|
1226
|
+
unless filter.nil? || filter.is_a?(ExpectationFilter)
|
|
1227
|
+
raise ArgumentError, "filter must be a MockServer::ExpectationFilter, got #{filter.class}"
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1178
1230
|
body = request ? JSON.generate(request.to_h) : ''
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
)
|
|
1231
|
+
query_params = { 'type' => 'ACTIVE_EXPECTATIONS', 'format' => 'JSON' }
|
|
1232
|
+
query_params.merge!(filter.to_query_params) if filter
|
|
1233
|
+
status, response_body = do_request('PUT', '/mockserver/retrieve', body, query_params)
|
|
1183
1234
|
if status >= 400
|
|
1184
1235
|
raise Error, "Failed to retrieve active expectations (status=#{status}): #{response_body}"
|
|
1185
1236
|
end
|
|
@@ -1191,14 +1242,47 @@ module MockServer
|
|
|
1191
1242
|
[]
|
|
1192
1243
|
end
|
|
1193
1244
|
|
|
1194
|
-
# Retrieve
|
|
1245
|
+
# Retrieve active expectations whose metadata has +key+ with a value
|
|
1246
|
+
# equal to +value+ (see {ExpectationFilter#with_metadata}).
|
|
1247
|
+
# @return [Array<Expectation>]
|
|
1248
|
+
def retrieve_active_expectations_by_metadata(key, value)
|
|
1249
|
+
retrieve_active_expectations(filter: ExpectationFilter.new.with_metadata(key, value))
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
# Retrieve active expectations whose metadata has +key+, with any value.
|
|
1253
|
+
# @return [Array<Expectation>]
|
|
1254
|
+
def retrieve_active_expectations_by_metadata_key(key)
|
|
1255
|
+
retrieve_active_expectations(filter: ExpectationFilter.new.with_metadata_key(key))
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
# Retrieve the active expectations that have not matched any request.
|
|
1259
|
+
# @return [Array<Expectation>]
|
|
1260
|
+
def retrieve_unmatched_expectations
|
|
1261
|
+
retrieve_active_expectations(filter: ExpectationFilter.new.with_unmatched(true))
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1264
|
+
# Retrieve recorded expectations, optionally one page of them in the
|
|
1265
|
+
# order the requests were received.
|
|
1195
1266
|
# @param request [HttpRequest, nil]
|
|
1267
|
+
# @param offset [Integer, nil] number to skip, zero or more
|
|
1268
|
+
# @param limit [Integer, nil] maximum number to return, zero or more
|
|
1269
|
+
# @param options [RecordedExpectationsOptions, nil] shapes the returned
|
|
1270
|
+
# expectations, for example replaying a request recorded with different
|
|
1271
|
+
# responses in order
|
|
1196
1272
|
# @return [Array<Expectation>]
|
|
1197
|
-
def retrieve_recorded_expectations(request: nil)
|
|
1273
|
+
def retrieve_recorded_expectations(request: nil, offset: nil, limit: nil, options: nil)
|
|
1274
|
+
unless options.nil? || options.is_a?(RecordedExpectationsOptions)
|
|
1275
|
+
raise ArgumentError, "options must be a RecordedExpectationsOptions, got #{options.inspect}"
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
paging = ExpectationFilter.new
|
|
1279
|
+
paging.with_offset(offset) unless offset.nil?
|
|
1280
|
+
paging.with_limit(limit) unless limit.nil?
|
|
1198
1281
|
body = request ? JSON.generate(request.to_h) : ''
|
|
1282
|
+
query_params = { 'type' => 'RECORDED_EXPECTATIONS', 'format' => 'JSON' }
|
|
1283
|
+
query_params.merge!(options.to_query_params) unless options.nil?
|
|
1199
1284
|
status, response_body = do_request(
|
|
1200
|
-
'PUT', '/mockserver/retrieve', body,
|
|
1201
|
-
{ 'type' => 'RECORDED_EXPECTATIONS', 'format' => 'JSON' }
|
|
1285
|
+
'PUT', '/mockserver/retrieve', body, query_params.merge(paging.to_query_params)
|
|
1202
1286
|
)
|
|
1203
1287
|
if status >= 400
|
|
1204
1288
|
raise Error, "Failed to retrieve recorded expectations (status=#{status}): #{response_body}"
|
|
@@ -1655,7 +1739,8 @@ module MockServer
|
|
|
1655
1739
|
context_path: @context_path,
|
|
1656
1740
|
secure: @secure,
|
|
1657
1741
|
ca_cert_path: @ca_cert_path,
|
|
1658
|
-
tls_verify: @tls_verify
|
|
1742
|
+
tls_verify: @tls_verify,
|
|
1743
|
+
authorization: @control_plane_authorization
|
|
1659
1744
|
)
|
|
1660
1745
|
ws_client.instance_variable_set(:@is_breakpoint_ws, true)
|
|
1661
1746
|
ws_client.listen
|
|
@@ -1672,7 +1757,8 @@ module MockServer
|
|
|
1672
1757
|
context_path: @context_path,
|
|
1673
1758
|
secure: @secure,
|
|
1674
1759
|
ca_cert_path: @ca_cert_path,
|
|
1675
|
-
tls_verify: @tls_verify
|
|
1760
|
+
tls_verify: @tls_verify,
|
|
1761
|
+
authorization: @control_plane_authorization
|
|
1676
1762
|
)
|
|
1677
1763
|
|
|
1678
1764
|
case callback_type
|
|
@@ -1687,6 +1773,26 @@ module MockServer
|
|
|
1687
1773
|
ws_client.client_id
|
|
1688
1774
|
end
|
|
1689
1775
|
|
|
1776
|
+
# @api private
|
|
1777
|
+
def clear_with_filter(request, type, filter)
|
|
1778
|
+
unless filter.is_a?(ExpectationFilter) && filter.selection?
|
|
1779
|
+
raise ArgumentError, 'clear with a filter requires a metadata or unmatched selection; ' \
|
|
1780
|
+
'use clear(request) to clear without one'
|
|
1781
|
+
end
|
|
1782
|
+
raise ArgumentError, 'offset and limit are not supported for clear' if filter.paging?
|
|
1783
|
+
unless type.nil? || type.to_s.casecmp?('expectations')
|
|
1784
|
+
raise ArgumentError, "clear with a filter only clears expectations, not type #{type.inspect}"
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
1788
|
+
status, response_body = do_request(
|
|
1789
|
+
'PUT', '/mockserver/clear', body, { 'type' => 'expectations' }.merge(filter.to_query_params)
|
|
1790
|
+
)
|
|
1791
|
+
raise Error, "Failed to clear (status=#{status}): #{response_body}" if status >= 400
|
|
1792
|
+
|
|
1793
|
+
nil
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1690
1796
|
# Perform an HTTP request with optional query parameters.
|
|
1691
1797
|
# @api private
|
|
1692
1798
|
def do_request(method, path, body = nil, query_params = nil,
|
|
@@ -1784,10 +1890,23 @@ module MockServer
|
|
|
1784
1890
|
req = Net::HTTP::Put.new(request_path)
|
|
1785
1891
|
end
|
|
1786
1892
|
req['Content-Type'] = content_type
|
|
1893
|
+
req['Authorization'] = @control_plane_authorization if @control_plane_authorization
|
|
1787
1894
|
req.body = body if body
|
|
1788
1895
|
req
|
|
1789
1896
|
end
|
|
1790
1897
|
|
|
1898
|
+
# @api private
|
|
1899
|
+
def basic_authorization(credentials)
|
|
1900
|
+
return nil if credentials.nil?
|
|
1901
|
+
|
|
1902
|
+
username, password = credentials
|
|
1903
|
+
if username.nil? || username.to_s.empty? || username.to_s.include?(':') || password.nil?
|
|
1904
|
+
raise ArgumentError, 'control_plane_basic_auth needs [username, password] with a username without ":"'
|
|
1905
|
+
end
|
|
1906
|
+
|
|
1907
|
+
"Basic #{Base64.strict_encode64("#{username}:#{password}".encode('UTF-8'))}"
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1791
1910
|
# @api private
|
|
1792
1911
|
def execute_request(http, req)
|
|
1793
1912
|
# Use the block form of #start so the underlying TCP/TLS connection is
|
|
@@ -37,6 +37,14 @@ module MockServer
|
|
|
37
37
|
self
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Set the expectation metadata, used to retrieve or clear it by metadata.
|
|
41
|
+
# @param metadata [Hash] String (or Symbol) keys to any JSON value
|
|
42
|
+
# @return [self]
|
|
43
|
+
def with_metadata(metadata)
|
|
44
|
+
@expectation.metadata = metadata
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
# Set the response action. Accepts an HttpResponse, HttpTemplate, or
|
|
41
49
|
# a Proc/lambda callback.
|
|
42
50
|
# @param response_or_callback [HttpResponse, HttpTemplate, Proc]
|
data/lib/mockserver/mcp.rb
CHANGED
|
@@ -31,20 +31,37 @@ module MockServer
|
|
|
31
31
|
quoted[1...-1]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
VELOCITY_METACHARACTER_ESCAPES = { '$' => '${esc.d}', '#' => '${esc.h}' }.freeze
|
|
35
|
+
private_constant :VELOCITY_METACHARACTER_ESCAPES
|
|
36
|
+
|
|
34
37
|
# Escape Velocity metacharacters so literal +$+ / +#+ survive rendering.
|
|
38
|
+
# Velocity consumes backslashes written before a reference or directive, so
|
|
39
|
+
# a run of backslashes followed by +$+, +#+ or the end of the value is
|
|
40
|
+
# escaped too; other backslashes are literal to Velocity and left as-is.
|
|
35
41
|
# @api private
|
|
36
42
|
def self.escape_velocity(value)
|
|
37
43
|
return value if value.nil?
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
# group 2 consumes the metacharacter after a run, is empty at the end and
|
|
46
|
+
# unset (empty alternative) otherwise, so no lookaround is needed
|
|
47
|
+
value.to_s.gsub(/(\\+)(?:([$#]|\z)|)|[$#]/) do |match|
|
|
48
|
+
run = Regexp.last_match(1)
|
|
49
|
+
next VELOCITY_METACHARACTER_ESCAPES[match] if run.nil?
|
|
50
|
+
|
|
51
|
+
following = Regexp.last_match(2)
|
|
52
|
+
next run if following.nil?
|
|
53
|
+
|
|
54
|
+
('${esc.b}' * run.length) + VELOCITY_METACHARACTER_ESCAPES.fetch(following, '')
|
|
55
|
+
end
|
|
40
56
|
end
|
|
41
57
|
|
|
42
|
-
# Escape single quotes for safe inclusion inside a
|
|
58
|
+
# Escape backslashes, then single quotes, for safe inclusion inside a
|
|
59
|
+
# single-quoted JSONPath string literal.
|
|
43
60
|
# @api private
|
|
44
61
|
def self.escape_json_path(value)
|
|
45
62
|
return '' if value.nil?
|
|
46
63
|
|
|
47
|
-
value.to_s.gsub("'"
|
|
64
|
+
value.to_s.gsub('\\') { '\\\\' }.gsub("'") { "\\'" }
|
|
48
65
|
end
|
|
49
66
|
|
|
50
67
|
# Parse + re-serialize JSON (compact) to validate it. Raises on invalid JSON.
|