mockserver-client 6.0.0 → 7.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52c92809897fcdfe317afdab740419e5bef114b46482df4a9756821916bd4a73
4
- data.tar.gz: 8b8ee1a6a5c341edecbe70a90ccb1dda4c481162eca2f30e83ee121beaa656d5
3
+ metadata.gz: 97517808097f2af911f727db3decbafcab8454285325b141ad5899a53af70be1
4
+ data.tar.gz: 60b5bff4ef90906b229064f23b1593bec547dc428b9d93c61b387d29a2615dd0
5
5
  SHA512:
6
- metadata.gz: 476994411a429882a93e0fe6852527b14ac9ce60ffc2ac328f0536eef2adee69f7c2ce8061bc8dfda7cc32869fe0e227ed3f5e979a3a1e1513c1ae3540be2f27
7
- data.tar.gz: 94828363b6159101e0288c15b604e7e9acf19716f67efb5edf56f591a79946aaf987ed2c32f079f421d3ad74045d61fa0aac62b93820d162df3299b284108cd7
6
+ metadata.gz: d91bb153b43f523a60812dc4f100fb1e7deb04c6ddc6353266d632f8f074df3683e0292d9cd77efb464034e618d4e4d63fafc3b11b333c009098cc5b48b49684
7
+ data.tar.gz: 7484d0dfa3e38edb7ad83dd2bb760a2b17dd2c4f474538e5673630f9fd28c056f4f7c2d2389ddbda2aa56f43eb052915cb4b5a432b4f15e7594e4db54cf92c9f
@@ -148,6 +148,118 @@ module MockServer
148
148
  close
149
149
  end
150
150
 
151
+ # -------------------------------------------------------------------
152
+ # Clock Control
153
+ # -------------------------------------------------------------------
154
+
155
+ # Freeze the server clock at the given ISO-8601 instant.
156
+ # If +instant+ is nil, the clock freezes at the current real time.
157
+ # @param instant [String, nil] ISO-8601 instant (e.g. "2025-01-15T09:30:00Z")
158
+ # @return [Hash] response with status, currentInstant, currentEpochMillis
159
+ def freeze_clock(instant = nil)
160
+ payload = { 'action' => 'freeze' }
161
+ payload['instant'] = instant if instant
162
+ body = JSON.generate(payload)
163
+ status, response_body = request('PUT', '/mockserver/clock', body)
164
+ if status >= 400
165
+ raise Error, "Failed to freeze clock (status=#{status}): #{response_body}"
166
+ end
167
+
168
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
169
+ end
170
+
171
+ # Advance the frozen clock by +duration_millis+ milliseconds.
172
+ # @param duration_millis [Integer]
173
+ # @return [Hash] response with status, currentInstant, currentEpochMillis
174
+ def advance_clock(duration_millis)
175
+ body = JSON.generate({ 'action' => 'advance', 'durationMillis' => duration_millis })
176
+ status, response_body = request('PUT', '/mockserver/clock', body)
177
+ if status >= 400
178
+ raise Error, "Failed to advance clock (status=#{status}): #{response_body}"
179
+ end
180
+
181
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
182
+ end
183
+
184
+ # Reset the server clock to real wall-clock time.
185
+ # @return [Hash] response with status, currentInstant, currentEpochMillis
186
+ def reset_clock
187
+ body = JSON.generate({ 'action' => 'reset' })
188
+ status, response_body = request('PUT', '/mockserver/clock', body)
189
+ if status >= 400
190
+ raise Error, "Failed to reset clock (status=#{status}): #{response_body}"
191
+ end
192
+
193
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
194
+ end
195
+
196
+ # Query the current clock status.
197
+ # @return [Hash] with currentInstant, currentEpochMillis, frozen
198
+ def clock_status
199
+ status, response_body = request('GET', '/mockserver/clock')
200
+ if status >= 400
201
+ raise Error, "Failed to get clock status (status=#{status}): #{response_body}"
202
+ end
203
+
204
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
205
+ end
206
+
207
+ # Register a service-scoped HTTP chaos profile for an upstream host. The profile
208
+ # is applied to every matched forward expectation to that host that does not
209
+ # define its own chaos (an expectation's own chaos always wins). The host is
210
+ # matched case-insensitively, ignoring any +:port+.
211
+ # @param host [String] the upstream host to break
212
+ # @param chaos [HttpChaosProfile] the chaos profile to apply
213
+ # @param ttl_millis [Integer, nil] if set, the chaos auto-reverts after this many ms
214
+ # @return [Hash] response with status and host
215
+ def set_service_chaos(host, chaos, ttl_millis = nil)
216
+ payload = { 'host' => host, 'chaos' => chaos.to_h }
217
+ payload['ttlMillis'] = ttl_millis unless ttl_millis.nil?
218
+ body = JSON.generate(payload)
219
+ status, response_body = request('PUT', '/mockserver/serviceChaos', body)
220
+ if status >= 400
221
+ raise Error, "Failed to set service chaos (status=#{status}): #{response_body}"
222
+ end
223
+
224
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
225
+ end
226
+
227
+ # Remove the service-scoped chaos profile registered for +host+.
228
+ # @param host [String]
229
+ # @return [Hash]
230
+ def remove_service_chaos(host)
231
+ body = JSON.generate({ 'host' => host, 'remove' => true })
232
+ status, response_body = request('PUT', '/mockserver/serviceChaos', body)
233
+ if status >= 400
234
+ raise Error, "Failed to remove service chaos (status=#{status}): #{response_body}"
235
+ end
236
+
237
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
238
+ end
239
+
240
+ # Clear all service-scoped chaos profiles.
241
+ # @return [Hash]
242
+ def clear_service_chaos
243
+ body = JSON.generate({ 'clear' => true })
244
+ status, response_body = request('PUT', '/mockserver/serviceChaos', body)
245
+ if status >= 400
246
+ raise Error, "Failed to clear service chaos (status=#{status}): #{response_body}"
247
+ end
248
+
249
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
250
+ end
251
+
252
+ # Query the current service-scoped chaos registrations.
253
+ # @return [Hash] of the form { "services" => { host => profile, ... } }
254
+ def service_chaos_status
255
+ status, response_body = request('GET', '/mockserver/serviceChaos')
256
+ if status >= 400
257
+ raise Error, "Failed to get service chaos (status=#{status}): #{response_body}"
258
+ end
259
+
260
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
261
+ end
262
+
151
263
  # Verify that a request was received.
152
264
  # @param request [HttpRequest]
153
265
  # @param times [VerificationTimes, nil]
@@ -29,6 +29,14 @@ module MockServer
29
29
  self
30
30
  end
31
31
 
32
+ # Set a declarative HTTP chaos/fault injection profile.
33
+ # @param chaos [HttpChaosProfile]
34
+ # @return [self]
35
+ def with_chaos(chaos)
36
+ @expectation.chaos = chaos
37
+ self
38
+ end
39
+
32
40
  # Set the response action. Accepts an HttpResponse, HttpTemplate, or
33
41
  # a Proc/lambda callback.
34
42
  # @param response_or_callback [HttpResponse, HttpTemplate, Proc]
@@ -113,5 +121,92 @@ module MockServer
113
121
  @expectation.http_websocket_response = websocket_response
114
122
  @client.upsert(@expectation)
115
123
  end
124
+
125
+ # Set a gRPC stream response action.
126
+ # @param grpc_stream_response [GrpcStreamResponse]
127
+ # @return [Array<Expectation>]
128
+ def respond_with_grpc_stream(grpc_stream_response)
129
+ unless grpc_stream_response.is_a?(GrpcStreamResponse)
130
+ raise TypeError,
131
+ "Expected GrpcStreamResponse, got #{grpc_stream_response.class.name}"
132
+ end
133
+ @expectation.grpc_stream_response = grpc_stream_response
134
+ @client.upsert(@expectation)
135
+ end
136
+
137
+ # Set a gRPC bidi streaming response action.
138
+ # @param grpc_bidi_response [GrpcBidiResponse]
139
+ # @return [Array<Expectation>]
140
+ def respond_with_grpc_bidi(grpc_bidi_response)
141
+ unless grpc_bidi_response.is_a?(GrpcBidiResponse)
142
+ raise TypeError,
143
+ "Expected GrpcBidiResponse, got #{grpc_bidi_response.class.name}"
144
+ end
145
+ @expectation.grpc_bidi_response = grpc_bidi_response
146
+ @client.upsert(@expectation)
147
+ end
148
+
149
+ # Set a binary response action.
150
+ # @param binary_response [BinaryResponse]
151
+ # @return [Array<Expectation>]
152
+ def respond_with_binary(binary_response)
153
+ unless binary_response.is_a?(BinaryResponse)
154
+ raise TypeError,
155
+ "Expected BinaryResponse, got #{binary_response.class.name}"
156
+ end
157
+ @expectation.binary_response = binary_response
158
+ @client.upsert(@expectation)
159
+ end
160
+
161
+ # Set a DNS response action.
162
+ # @param dns_response [DnsResponse]
163
+ # @return [Array<Expectation>]
164
+ def respond_with_dns(dns_response)
165
+ unless dns_response.is_a?(DnsResponse)
166
+ raise TypeError,
167
+ "Expected DnsResponse, got #{dns_response.class.name}"
168
+ end
169
+ @expectation.dns_response = dns_response
170
+ @client.upsert(@expectation)
171
+ end
172
+
173
+ # Set a forward template action.
174
+ # @param template [HttpTemplate]
175
+ # @return [Array<Expectation>]
176
+ def forward_with_template(template)
177
+ unless template.is_a?(HttpTemplate)
178
+ raise TypeError,
179
+ "Expected HttpTemplate, got #{template.class.name}"
180
+ end
181
+ @expectation.http_forward_template = template
182
+ @client.upsert(@expectation)
183
+ end
184
+
185
+ # Set a forward class callback action.
186
+ # @param class_callback [HttpClassCallback]
187
+ # @return [Array<Expectation>]
188
+ def forward_with_class_callback(class_callback)
189
+ unless class_callback.is_a?(HttpClassCallback)
190
+ raise TypeError,
191
+ "Expected HttpClassCallback, got #{class_callback.class.name}"
192
+ end
193
+ @expectation.http_forward_class_callback = class_callback
194
+ @client.upsert(@expectation)
195
+ end
196
+
197
+ # Set an ordered multi-action pipeline of steps.
198
+ #
199
+ # Exactly one step must have +responder: true+; that step produces the
200
+ # HTTP response. All other steps are side-effects executed in order.
201
+ # @param steps [Array<ExpectationStep>]
202
+ # @return [Array<Expectation>]
203
+ def with_steps(steps)
204
+ unless steps.is_a?(Array) && steps.all? { |s| s.is_a?(ExpectationStep) }
205
+ raise TypeError, 'Expected an Array of ExpectationStep objects'
206
+ end
207
+
208
+ @expectation.steps = steps
209
+ @client.upsert(@expectation)
210
+ end
116
211
  end
117
212
  end
@@ -61,7 +61,27 @@ module MockServer
61
61
  'base_path' => 'basePath',
62
62
  'id_field' => 'idField',
63
63
  'id_strategy' => 'idStrategy',
64
- 'initial_data' => 'initialData'
64
+ 'initial_data' => 'initialData',
65
+ 'error_status' => 'errorStatus',
66
+ 'error_probability' => 'errorProbability',
67
+ 'drop_connection_probability' => 'dropConnectionProbability',
68
+ 'retry_after' => 'retryAfter',
69
+ 'succeed_first' => 'succeedFirst',
70
+ 'fail_request_count' => 'failRequestCount',
71
+ 'outage_after_millis' => 'outageAfterMillis',
72
+ 'outage_duration_millis' => 'outageDurationMillis',
73
+ 'truncate_body_at_fraction' => 'truncateBodyAtFraction',
74
+ 'malformed_body' => 'malformedBody',
75
+ 'slow_response_chunk_size' => 'slowResponseChunkSize',
76
+ 'slow_response_chunk_delay' => 'slowResponseChunkDelay',
77
+ 'quota_name' => 'quotaName',
78
+ 'quota_limit' => 'quotaLimit',
79
+ 'quota_window_millis' => 'quotaWindowMillis',
80
+ 'quota_error_status' => 'quotaErrorStatus',
81
+ 'degradation_ramp_millis' => 'degradationRampMillis',
82
+ 'http_class_callback' => 'httpClassCallback',
83
+ 'http_object_callback' => 'httpObjectCallback',
84
+ 'failure_policy' => 'failurePolicy'
65
85
  }.freeze
66
86
 
67
87
  REVERSE_FIELD_MAP = FIELD_MAP.invert.freeze
@@ -1146,14 +1166,396 @@ module MockServer
1146
1166
  end
1147
1167
  end
1148
1168
 
1169
+ class GrpcStreamMessage
1170
+ attr_accessor :json, :delay
1171
+
1172
+ def initialize(json: nil, delay: nil)
1173
+ @json = json
1174
+ @delay = delay
1175
+ end
1176
+
1177
+ def to_h
1178
+ result = {}
1179
+ result['json'] = @json unless @json.nil?
1180
+ result['delay'] = @delay.to_h if @delay
1181
+ result
1182
+ end
1183
+
1184
+ def self.from_hash(data)
1185
+ return nil if data.nil?
1186
+
1187
+ new(
1188
+ json: data['json'],
1189
+ delay: Delay.from_hash(data['delay'])
1190
+ )
1191
+ end
1192
+ end
1193
+
1194
+ class GrpcStreamResponse
1195
+ attr_accessor :status_name, :status_message, :headers, :messages,
1196
+ :close_connection, :delay, :primary
1197
+
1198
+ def initialize(status_name: nil, status_message: nil, headers: nil,
1199
+ messages: nil, close_connection: nil, delay: nil, primary: nil)
1200
+ @status_name = status_name
1201
+ @status_message = status_message
1202
+ @headers = headers
1203
+ @messages = messages
1204
+ @close_connection = close_connection
1205
+ @delay = delay
1206
+ @primary = primary
1207
+ end
1208
+
1209
+ def to_h
1210
+ result = {}
1211
+ result['statusName'] = @status_name unless @status_name.nil?
1212
+ result['statusMessage'] = @status_message unless @status_message.nil?
1213
+ result['headers'] = MockServer.serialize_key_multi_values(@headers) if @headers
1214
+ result['messages'] = @messages&.map(&:to_h) if @messages
1215
+ result['closeConnection'] = @close_connection unless @close_connection.nil?
1216
+ result['delay'] = @delay.to_h if @delay
1217
+ result['primary'] = @primary unless @primary.nil?
1218
+ result
1219
+ end
1220
+
1221
+ def self.from_hash(data)
1222
+ return nil if data.nil?
1223
+
1224
+ messages_data = data['messages']
1225
+ messages = messages_data&.map { |m| GrpcStreamMessage.from_hash(m) }
1226
+ new(
1227
+ status_name: data['statusName'],
1228
+ status_message: data['statusMessage'],
1229
+ headers: MockServer.deserialize_key_multi_values(data['headers']),
1230
+ messages: messages,
1231
+ close_connection: data['closeConnection'],
1232
+ delay: Delay.from_hash(data['delay']),
1233
+ primary: data['primary']
1234
+ )
1235
+ end
1236
+ end
1237
+
1238
+ class GrpcBidiRule
1239
+ attr_accessor :match_json, :responses
1240
+
1241
+ def initialize(match_json: nil, responses: nil)
1242
+ @match_json = match_json
1243
+ @responses = responses
1244
+ end
1245
+
1246
+ def to_h
1247
+ result = {}
1248
+ result['matchJson'] = @match_json unless @match_json.nil?
1249
+ result['responses'] = @responses&.map(&:to_h) if @responses
1250
+ result
1251
+ end
1252
+
1253
+ def self.from_hash(data)
1254
+ return nil if data.nil?
1255
+
1256
+ responses_data = data['responses']
1257
+ responses = responses_data&.map { |r| GrpcStreamMessage.from_hash(r) }
1258
+ new(
1259
+ match_json: data['matchJson'],
1260
+ responses: responses
1261
+ )
1262
+ end
1263
+ end
1264
+
1265
+ class GrpcBidiResponse
1266
+ attr_accessor :status_name, :status_message, :headers, :messages,
1267
+ :rules, :close_connection, :delay, :primary
1268
+
1269
+ def initialize(status_name: nil, status_message: nil, headers: nil,
1270
+ messages: nil, rules: nil, close_connection: nil, delay: nil, primary: nil)
1271
+ @status_name = status_name
1272
+ @status_message = status_message
1273
+ @headers = headers
1274
+ @messages = messages
1275
+ @rules = rules
1276
+ @close_connection = close_connection
1277
+ @delay = delay
1278
+ @primary = primary
1279
+ end
1280
+
1281
+ def to_h
1282
+ result = {}
1283
+ result['statusName'] = @status_name unless @status_name.nil?
1284
+ result['statusMessage'] = @status_message unless @status_message.nil?
1285
+ result['headers'] = MockServer.serialize_key_multi_values(@headers) if @headers
1286
+ result['messages'] = @messages&.map(&:to_h) if @messages
1287
+ result['rules'] = @rules&.map(&:to_h) if @rules
1288
+ result['closeConnection'] = @close_connection unless @close_connection.nil?
1289
+ result['delay'] = @delay.to_h if @delay
1290
+ result['primary'] = @primary unless @primary.nil?
1291
+ result
1292
+ end
1293
+
1294
+ def self.from_hash(data)
1295
+ return nil if data.nil?
1296
+
1297
+ messages_data = data['messages']
1298
+ messages = messages_data&.map { |m| GrpcStreamMessage.from_hash(m) }
1299
+ rules_data = data['rules']
1300
+ rules = rules_data&.map { |r| GrpcBidiRule.from_hash(r) }
1301
+ new(
1302
+ status_name: data['statusName'],
1303
+ status_message: data['statusMessage'],
1304
+ headers: MockServer.deserialize_key_multi_values(data['headers']),
1305
+ messages: messages,
1306
+ rules: rules,
1307
+ close_connection: data['closeConnection'],
1308
+ delay: Delay.from_hash(data['delay']),
1309
+ primary: data['primary']
1310
+ )
1311
+ end
1312
+ end
1313
+
1314
+ class BinaryResponse
1315
+ attr_accessor :binary_data, :delay, :primary
1316
+
1317
+ def initialize(binary_data: nil, delay: nil, primary: nil)
1318
+ @binary_data = binary_data
1319
+ @delay = delay
1320
+ @primary = primary
1321
+ end
1322
+
1323
+ def to_h
1324
+ result = {}
1325
+ result['binaryData'] = @binary_data unless @binary_data.nil?
1326
+ result['delay'] = @delay.to_h if @delay
1327
+ result['primary'] = @primary unless @primary.nil?
1328
+ result
1329
+ end
1330
+
1331
+ def self.from_hash(data)
1332
+ return nil if data.nil?
1333
+
1334
+ new(
1335
+ binary_data: data['binaryData'],
1336
+ delay: Delay.from_hash(data['delay']),
1337
+ primary: data['primary']
1338
+ )
1339
+ end
1340
+ end
1341
+
1342
+ class DnsRecord
1343
+ attr_accessor :name, :type, :dns_class, :ttl, :value,
1344
+ :priority, :weight, :port
1345
+
1346
+ def initialize(name: nil, type: nil, dns_class: nil, ttl: nil,
1347
+ value: nil, priority: nil, weight: nil, port: nil)
1348
+ @name = name
1349
+ @type = type
1350
+ @dns_class = dns_class
1351
+ @ttl = ttl
1352
+ @value = value
1353
+ @priority = priority
1354
+ @weight = weight
1355
+ @port = port
1356
+ end
1357
+
1358
+ def to_h
1359
+ result = {}
1360
+ result['name'] = @name unless @name.nil?
1361
+ result['type'] = @type unless @type.nil?
1362
+ result['dnsClass'] = @dns_class unless @dns_class.nil?
1363
+ result['ttl'] = @ttl unless @ttl.nil?
1364
+ result['value'] = @value unless @value.nil?
1365
+ result['priority'] = @priority unless @priority.nil?
1366
+ result['weight'] = @weight unless @weight.nil?
1367
+ result['port'] = @port unless @port.nil?
1368
+ result
1369
+ end
1370
+
1371
+ def self.from_hash(data)
1372
+ return nil if data.nil?
1373
+
1374
+ new(
1375
+ name: data['name'],
1376
+ type: data['type'],
1377
+ dns_class: data['dnsClass'],
1378
+ ttl: data['ttl'],
1379
+ value: data['value'],
1380
+ priority: data['priority'],
1381
+ weight: data['weight'],
1382
+ port: data['port']
1383
+ )
1384
+ end
1385
+
1386
+ def self.a_record(name, ip)
1387
+ new(name: name, type: 'A', value: ip)
1388
+ end
1389
+
1390
+ def self.aaaa_record(name, ip)
1391
+ new(name: name, type: 'AAAA', value: ip)
1392
+ end
1393
+
1394
+ def self.cname_record(name, cname)
1395
+ new(name: name, type: 'CNAME', value: cname)
1396
+ end
1397
+
1398
+ def self.mx_record(name, priority, exchange)
1399
+ new(name: name, type: 'MX', priority: priority, value: exchange)
1400
+ end
1401
+
1402
+ def self.srv_record(name, priority, weight, port, target)
1403
+ new(name: name, type: 'SRV', priority: priority, weight: weight, port: port, value: target)
1404
+ end
1405
+
1406
+ def self.txt_record(name, text)
1407
+ new(name: name, type: 'TXT', value: text)
1408
+ end
1409
+
1410
+ def self.ptr_record(name, pointer)
1411
+ new(name: name, type: 'PTR', value: pointer)
1412
+ end
1413
+ end
1414
+
1415
+ class DnsResponse
1416
+ attr_accessor :response_code, :answer_records, :authority_records,
1417
+ :additional_records, :delay, :primary
1418
+
1419
+ def initialize(response_code: nil, answer_records: nil, authority_records: nil,
1420
+ additional_records: nil, delay: nil, primary: nil)
1421
+ @response_code = response_code
1422
+ @answer_records = answer_records
1423
+ @authority_records = authority_records
1424
+ @additional_records = additional_records
1425
+ @delay = delay
1426
+ @primary = primary
1427
+ end
1428
+
1429
+ def to_h
1430
+ result = {}
1431
+ result['responseCode'] = @response_code unless @response_code.nil?
1432
+ result['answerRecords'] = @answer_records.map(&:to_h) if @answer_records
1433
+ result['authorityRecords'] = @authority_records.map(&:to_h) if @authority_records
1434
+ result['additionalRecords'] = @additional_records.map(&:to_h) if @additional_records
1435
+ result['delay'] = @delay.to_h if @delay
1436
+ result['primary'] = @primary unless @primary.nil?
1437
+ result
1438
+ end
1439
+
1440
+ def self.from_hash(data)
1441
+ return nil if data.nil?
1442
+
1443
+ answer_data = data['answerRecords']
1444
+ authority_data = data['authorityRecords']
1445
+ additional_data = data['additionalRecords']
1446
+ new(
1447
+ response_code: data['responseCode'],
1448
+ answer_records: answer_data&.map { |r| DnsRecord.from_hash(r) },
1449
+ authority_records: authority_data&.map { |r| DnsRecord.from_hash(r) },
1450
+ additional_records: additional_data&.map { |r| DnsRecord.from_hash(r) },
1451
+ delay: Delay.from_hash(data['delay']),
1452
+ primary: data['primary']
1453
+ )
1454
+ end
1455
+ end
1456
+
1457
+ class HttpChaosProfile
1458
+ attr_accessor :error_status, :error_probability, :drop_connection_probability,
1459
+ :retry_after, :latency, :seed, :succeed_first, :fail_request_count,
1460
+ :outage_after_millis, :outage_duration_millis,
1461
+ :truncate_body_at_fraction, :malformed_body,
1462
+ :slow_response_chunk_size, :slow_response_chunk_delay,
1463
+ :quota_name, :quota_limit, :quota_window_millis, :quota_error_status,
1464
+ :degradation_ramp_millis
1465
+
1466
+ def initialize(error_status: nil, error_probability: nil, drop_connection_probability: nil,
1467
+ retry_after: nil, latency: nil, seed: nil, succeed_first: nil, fail_request_count: nil,
1468
+ outage_after_millis: nil, outage_duration_millis: nil,
1469
+ truncate_body_at_fraction: nil, malformed_body: nil,
1470
+ slow_response_chunk_size: nil, slow_response_chunk_delay: nil,
1471
+ quota_name: nil, quota_limit: nil, quota_window_millis: nil, quota_error_status: nil,
1472
+ degradation_ramp_millis: nil)
1473
+ @error_status = error_status
1474
+ @error_probability = error_probability
1475
+ @drop_connection_probability = drop_connection_probability
1476
+ @retry_after = retry_after
1477
+ @latency = latency
1478
+ @seed = seed
1479
+ @succeed_first = succeed_first
1480
+ @fail_request_count = fail_request_count
1481
+ @outage_after_millis = outage_after_millis
1482
+ @outage_duration_millis = outage_duration_millis
1483
+ @truncate_body_at_fraction = truncate_body_at_fraction
1484
+ @malformed_body = malformed_body
1485
+ @slow_response_chunk_size = slow_response_chunk_size
1486
+ @slow_response_chunk_delay = slow_response_chunk_delay
1487
+ @quota_name = quota_name
1488
+ @quota_limit = quota_limit
1489
+ @quota_window_millis = quota_window_millis
1490
+ @quota_error_status = quota_error_status
1491
+ @degradation_ramp_millis = degradation_ramp_millis
1492
+ end
1493
+
1494
+ def to_h
1495
+ MockServer.strip_none({
1496
+ 'errorStatus' => @error_status,
1497
+ 'errorProbability' => @error_probability,
1498
+ 'dropConnectionProbability' => @drop_connection_probability,
1499
+ 'retryAfter' => @retry_after,
1500
+ 'latency' => @latency&.to_h,
1501
+ 'seed' => @seed,
1502
+ 'succeedFirst' => @succeed_first,
1503
+ 'failRequestCount' => @fail_request_count,
1504
+ 'outageAfterMillis' => @outage_after_millis,
1505
+ 'outageDurationMillis' => @outage_duration_millis,
1506
+ 'truncateBodyAtFraction' => @truncate_body_at_fraction,
1507
+ 'malformedBody' => @malformed_body,
1508
+ 'slowResponseChunkSize' => @slow_response_chunk_size,
1509
+ 'slowResponseChunkDelay' => @slow_response_chunk_delay&.to_h,
1510
+ 'quotaName' => @quota_name,
1511
+ 'quotaLimit' => @quota_limit,
1512
+ 'quotaWindowMillis' => @quota_window_millis,
1513
+ 'quotaErrorStatus' => @quota_error_status,
1514
+ 'degradationRampMillis' => @degradation_ramp_millis
1515
+ })
1516
+ end
1517
+
1518
+ def self.from_hash(data)
1519
+ return nil if data.nil?
1520
+
1521
+ new(
1522
+ error_status: data['errorStatus'],
1523
+ error_probability: data['errorProbability'],
1524
+ drop_connection_probability: data['dropConnectionProbability'],
1525
+ retry_after: data['retryAfter'],
1526
+ latency: Delay.from_hash(data['latency']),
1527
+ seed: data['seed'],
1528
+ succeed_first: data['succeedFirst'],
1529
+ fail_request_count: data['failRequestCount'],
1530
+ outage_after_millis: data['outageAfterMillis'],
1531
+ outage_duration_millis: data['outageDurationMillis'],
1532
+ truncate_body_at_fraction: data['truncateBodyAtFraction'],
1533
+ malformed_body: data['malformedBody'],
1534
+ slow_response_chunk_size: data['slowResponseChunkSize'],
1535
+ slow_response_chunk_delay: Delay.from_hash(data['slowResponseChunkDelay']),
1536
+ quota_name: data['quotaName'],
1537
+ quota_limit: data['quotaLimit'],
1538
+ quota_window_millis: data['quotaWindowMillis'],
1539
+ quota_error_status: data['quotaErrorStatus'],
1540
+ degradation_ramp_millis: data['degradationRampMillis']
1541
+ )
1542
+ end
1543
+ end
1544
+
1149
1545
  class AfterAction
1150
- attr_accessor :http_request, :http_class_callback, :http_object_callback, :delay
1546
+ # blocking, timeout and failure_policy are only meaningful for before-actions
1547
+ attr_accessor :http_request, :http_class_callback, :http_object_callback, :delay,
1548
+ :blocking, :timeout, :failure_policy
1151
1549
 
1152
- def initialize(http_request: nil, http_class_callback: nil, http_object_callback: nil, delay: nil)
1550
+ def initialize(http_request: nil, http_class_callback: nil, http_object_callback: nil, delay: nil,
1551
+ blocking: nil, timeout: nil, failure_policy: nil)
1153
1552
  @http_request = http_request
1154
1553
  @http_class_callback = http_class_callback
1155
1554
  @http_object_callback = http_object_callback
1156
1555
  @delay = delay
1556
+ @blocking = blocking
1557
+ @timeout = timeout
1558
+ @failure_policy = failure_policy
1157
1559
  end
1158
1560
 
1159
1561
  def to_h
@@ -1161,7 +1563,10 @@ module MockServer
1161
1563
  'httpRequest' => @http_request&.to_h,
1162
1564
  'httpClassCallback' => @http_class_callback&.to_h,
1163
1565
  'httpObjectCallback' => @http_object_callback&.to_h,
1164
- 'delay' => @delay&.to_h
1566
+ 'delay' => @delay&.to_h,
1567
+ 'blocking' => @blocking,
1568
+ 'timeout' => @timeout&.to_h,
1569
+ 'failurePolicy' => @failure_policy
1165
1570
  })
1166
1571
  end
1167
1572
 
@@ -1172,7 +1577,77 @@ module MockServer
1172
1577
  http_request: HttpRequest.from_hash(data['httpRequest']),
1173
1578
  http_class_callback: HttpClassCallback.from_hash(data['httpClassCallback']),
1174
1579
  http_object_callback: HttpObjectCallback.from_hash(data['httpObjectCallback']),
1175
- delay: Delay.from_hash(data['delay'])
1580
+ delay: Delay.from_hash(data['delay']),
1581
+ blocking: data['blocking'],
1582
+ timeout: Delay.from_hash(data['timeout']),
1583
+ failure_policy: data['failurePolicy']
1584
+ )
1585
+ end
1586
+ end
1587
+
1588
+ # A single step in an ordered multi-action expectation pipeline.
1589
+ #
1590
+ # Each step carries exactly ONE action target and a +responder+ flag.
1591
+ # Steps without +responder = true+ are side-effects (fire-and-forget
1592
+ # webhooks/callbacks). Exactly one step in the list must be marked as the
1593
+ # responder; that step's action produces the HTTP response.
1594
+ class ExpectationStep
1595
+ attr_accessor :http_request, :http_class_callback, :http_object_callback,
1596
+ :http_forward, :http_override_forwarded_request,
1597
+ :http_response, :http_error,
1598
+ :responder, :delay, :blocking, :timeout, :failure_policy
1599
+
1600
+ def initialize(http_request: nil, http_class_callback: nil, http_object_callback: nil,
1601
+ http_forward: nil, http_override_forwarded_request: nil,
1602
+ http_response: nil, http_error: nil,
1603
+ responder: nil, delay: nil, blocking: nil, timeout: nil, failure_policy: nil)
1604
+ @http_request = http_request
1605
+ @http_class_callback = http_class_callback
1606
+ @http_object_callback = http_object_callback
1607
+ @http_forward = http_forward
1608
+ @http_override_forwarded_request = http_override_forwarded_request
1609
+ @http_response = http_response
1610
+ @http_error = http_error
1611
+ @responder = responder
1612
+ @delay = delay
1613
+ @blocking = blocking
1614
+ @timeout = timeout
1615
+ @failure_policy = failure_policy
1616
+ end
1617
+
1618
+ def to_h
1619
+ MockServer.strip_none({
1620
+ 'httpRequest' => @http_request&.to_h,
1621
+ 'httpClassCallback' => @http_class_callback&.to_h,
1622
+ 'httpObjectCallback' => @http_object_callback&.to_h,
1623
+ 'httpForward' => @http_forward&.to_h,
1624
+ 'httpOverrideForwardedRequest' => @http_override_forwarded_request&.to_h,
1625
+ 'httpResponse' => @http_response&.to_h,
1626
+ 'httpError' => @http_error&.to_h,
1627
+ 'responder' => @responder,
1628
+ 'delay' => @delay&.to_h,
1629
+ 'blocking' => @blocking,
1630
+ 'timeout' => @timeout&.to_h,
1631
+ 'failurePolicy' => @failure_policy
1632
+ })
1633
+ end
1634
+
1635
+ def self.from_hash(data)
1636
+ return nil if data.nil?
1637
+
1638
+ new(
1639
+ http_request: HttpRequest.from_hash(data['httpRequest']),
1640
+ http_class_callback: HttpClassCallback.from_hash(data['httpClassCallback']),
1641
+ http_object_callback: HttpObjectCallback.from_hash(data['httpObjectCallback']),
1642
+ http_forward: HttpForward.from_hash(data['httpForward']),
1643
+ http_override_forwarded_request: HttpOverrideForwardedRequest.from_hash(data['httpOverrideForwardedRequest']),
1644
+ http_response: HttpResponse.from_hash(data['httpResponse']),
1645
+ http_error: HttpError.from_hash(data['httpError']),
1646
+ responder: data['responder'],
1647
+ delay: Delay.from_hash(data['delay']),
1648
+ blocking: data['blocking'],
1649
+ timeout: Delay.from_hash(data['timeout']),
1650
+ failure_policy: data['failurePolicy']
1176
1651
  )
1177
1652
  end
1178
1653
  end
@@ -1183,9 +1658,12 @@ module MockServer
1183
1658
  :http_response_object_callback, :http_forward,
1184
1659
  :http_forward_template, :http_forward_class_callback,
1185
1660
  :http_forward_object_callback, :http_override_forwarded_request,
1186
- :http_error, :times, :time_to_live,
1187
- :http_sse_response, :http_websocket_response, :after_actions,
1188
- :http_responses, :response_mode,
1661
+ :http_error, :times, :time_to_live, :chaos,
1662
+ :http_sse_response, :http_websocket_response,
1663
+ :grpc_stream_response, :grpc_bidi_response,
1664
+ :binary_response, :dns_response,
1665
+ :before_actions, :after_actions,
1666
+ :http_responses, :response_mode, :steps,
1189
1667
  :scenario_name, :scenario_state, :new_scenario_state
1190
1668
 
1191
1669
  def initialize(id: nil, priority: nil, percentage: nil, http_request: nil, http_response: nil,
@@ -1193,9 +1671,12 @@ module MockServer
1193
1671
  http_response_object_callback: nil, http_forward: nil,
1194
1672
  http_forward_template: nil, http_forward_class_callback: nil,
1195
1673
  http_forward_object_callback: nil, http_override_forwarded_request: nil,
1196
- http_error: nil, times: nil, time_to_live: nil,
1197
- http_sse_response: nil, http_websocket_response: nil, after_actions: nil,
1198
- http_responses: nil, response_mode: nil,
1674
+ http_error: nil, times: nil, time_to_live: nil, chaos: nil,
1675
+ http_sse_response: nil, http_websocket_response: nil,
1676
+ grpc_stream_response: nil, grpc_bidi_response: nil,
1677
+ binary_response: nil, dns_response: nil,
1678
+ before_actions: nil, after_actions: nil,
1679
+ http_responses: nil, response_mode: nil, steps: nil,
1199
1680
  scenario_name: nil, scenario_state: nil, new_scenario_state: nil)
1200
1681
  @id = id
1201
1682
  @priority = priority
@@ -1213,17 +1694,31 @@ module MockServer
1213
1694
  @http_error = http_error
1214
1695
  @times = times
1215
1696
  @time_to_live = time_to_live
1697
+ @chaos = chaos
1216
1698
  @http_sse_response = http_sse_response
1217
1699
  @http_websocket_response = http_websocket_response
1700
+ @grpc_stream_response = grpc_stream_response
1701
+ @grpc_bidi_response = grpc_bidi_response
1702
+ @binary_response = binary_response
1703
+ @dns_response = dns_response
1704
+ @before_actions = before_actions
1218
1705
  @after_actions = after_actions
1219
1706
  @http_responses = http_responses
1220
1707
  @response_mode = response_mode
1708
+ @steps = steps
1221
1709
  @scenario_name = scenario_name
1222
1710
  @scenario_state = scenario_state
1223
1711
  @new_scenario_state = new_scenario_state
1224
1712
  end
1225
1713
 
1226
1714
  def to_h
1715
+ before_actions_h = nil
1716
+ if @before_actions.is_a?(Array)
1717
+ before_actions_h = @before_actions.map(&:to_h) unless @before_actions.empty?
1718
+ elsif @before_actions
1719
+ before_actions_h = @before_actions.to_h
1720
+ end
1721
+
1227
1722
  after_actions_h = nil
1228
1723
  if @after_actions.is_a?(Array)
1229
1724
  after_actions_h = @after_actions.map(&:to_h) unless @after_actions.empty?
@@ -1248,11 +1743,18 @@ module MockServer
1248
1743
  'httpError' => @http_error&.to_h,
1249
1744
  'httpSseResponse' => @http_sse_response&.to_h,
1250
1745
  'httpWebSocketResponse' => @http_websocket_response&.to_h,
1746
+ 'grpcStreamResponse' => @grpc_stream_response&.to_h,
1747
+ 'grpcBidiResponse' => @grpc_bidi_response&.to_h,
1748
+ 'binaryResponse' => @binary_response&.to_h,
1749
+ 'dnsResponse' => @dns_response&.to_h,
1750
+ 'beforeActions' => before_actions_h,
1251
1751
  'afterActions' => after_actions_h,
1252
1752
  'httpResponses' => @http_responses&.map(&:to_h),
1253
1753
  'responseMode' => @response_mode,
1754
+ 'steps' => @steps&.map(&:to_h),
1254
1755
  'times' => @times&.to_h,
1255
1756
  'timeToLive' => @time_to_live&.to_h,
1757
+ 'chaos' => @chaos&.to_h,
1256
1758
  'scenarioName' => @scenario_name,
1257
1759
  'scenarioState' => @scenario_state,
1258
1760
  'newScenarioState' => @new_scenario_state
@@ -1262,11 +1764,18 @@ module MockServer
1262
1764
  def self.from_hash(data)
1263
1765
  return nil if data.nil?
1264
1766
 
1767
+ before_actions_data = data['beforeActions']
1768
+ before_actions = if before_actions_data.is_a?(Array)
1769
+ before_actions_data.map { |a| AfterAction.from_hash(a) }
1770
+ elsif before_actions_data
1771
+ [AfterAction.from_hash(before_actions_data)]
1772
+ end
1773
+
1265
1774
  after_actions_data = data['afterActions']
1266
1775
  after_actions = if after_actions_data.is_a?(Array)
1267
1776
  after_actions_data.map { |a| AfterAction.from_hash(a) }
1268
1777
  elsif after_actions_data
1269
- AfterAction.from_hash(after_actions_data)
1778
+ [AfterAction.from_hash(after_actions_data)]
1270
1779
  end
1271
1780
 
1272
1781
  new(
@@ -1286,11 +1795,18 @@ module MockServer
1286
1795
  http_error: HttpError.from_hash(data['httpError']),
1287
1796
  http_sse_response: HttpSseResponse.from_hash(data['httpSseResponse']),
1288
1797
  http_websocket_response: HttpWebSocketResponse.from_hash(data['httpWebSocketResponse']),
1798
+ grpc_stream_response: GrpcStreamResponse.from_hash(data['grpcStreamResponse']),
1799
+ grpc_bidi_response: GrpcBidiResponse.from_hash(data['grpcBidiResponse']),
1800
+ binary_response: BinaryResponse.from_hash(data['binaryResponse']),
1801
+ dns_response: DnsResponse.from_hash(data['dnsResponse']),
1802
+ before_actions: before_actions,
1289
1803
  after_actions: after_actions,
1290
1804
  http_responses: data['httpResponses']&.map { |r| HttpResponse.from_hash(r) },
1291
1805
  response_mode: data['responseMode'],
1806
+ steps: data['steps']&.map { |s| ExpectationStep.from_hash(s) },
1292
1807
  times: Times.from_hash(data['times']),
1293
1808
  time_to_live: TimeToLive.from_hash(data['timeToLive']),
1809
+ chaos: HttpChaosProfile.from_hash(data['chaos']),
1294
1810
  scenario_name: data['scenarioName'],
1295
1811
  scenario_state: data['scenarioState'],
1296
1812
  new_scenario_state: data['newScenarioState']
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MockServer
4
- VERSION = '6.0.0'
4
+ VERSION = '7.0.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mockserver-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0
4
+ version: 7.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Bloom
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-21 00:00:00.000000000 Z
11
+ date: 2026-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger