mockserver-client 1.0.8.pre → 6.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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +2 -1
  3. data/README.md +79 -227
  4. data/lib/mockserver/client.rb +518 -0
  5. data/lib/mockserver/errors.rb +18 -0
  6. data/lib/mockserver/forward_chain_expectation.rb +117 -0
  7. data/lib/mockserver/models.rb +1507 -0
  8. data/lib/mockserver/version.rb +3 -3
  9. data/lib/mockserver/websocket_client.rb +353 -0
  10. data/lib/mockserver-client.rb +7 -16
  11. data/mockserver-client.gemspec +26 -27
  12. metadata +54 -206
  13. data/.gitignore +0 -21
  14. data/.rubocop.yml +0 -7
  15. data/Rakefile +0 -10
  16. data/bin/mockserver +0 -9
  17. data/lib/cli.rb +0 -146
  18. data/lib/mockserver/abstract_client.rb +0 -111
  19. data/lib/mockserver/mock_server_client.rb +0 -46
  20. data/lib/mockserver/model/array_of.rb +0 -85
  21. data/lib/mockserver/model/body.rb +0 -56
  22. data/lib/mockserver/model/cookie.rb +0 -36
  23. data/lib/mockserver/model/delay.rb +0 -34
  24. data/lib/mockserver/model/enum.rb +0 -47
  25. data/lib/mockserver/model/expectation.rb +0 -139
  26. data/lib/mockserver/model/forward.rb +0 -41
  27. data/lib/mockserver/model/header.rb +0 -43
  28. data/lib/mockserver/model/parameter.rb +0 -43
  29. data/lib/mockserver/model/request.rb +0 -81
  30. data/lib/mockserver/model/response.rb +0 -45
  31. data/lib/mockserver/model/times.rb +0 -61
  32. data/lib/mockserver/proxy_client.rb +0 -9
  33. data/lib/mockserver/utility_methods.rb +0 -59
  34. data/pom.xml +0 -118
  35. data/spec/fixtures/forward_mockserver.json +0 -7
  36. data/spec/fixtures/incorrect_login_response.json +0 -20
  37. data/spec/fixtures/post_login_request.json +0 -22
  38. data/spec/fixtures/register_expectation.json +0 -50
  39. data/spec/fixtures/retrieved_request.json +0 -22
  40. data/spec/fixtures/search_request.json +0 -6
  41. data/spec/fixtures/times_once.json +0 -6
  42. data/spec/integration/mock_client_integration_spec.rb +0 -82
  43. data/spec/mockserver/builder_spec.rb +0 -90
  44. data/spec/mockserver/mock_client_spec.rb +0 -80
  45. data/spec/mockserver/proxy_client_spec.rb +0 -38
  46. data/spec/spec_helper.rb +0 -61
@@ -1,5 +1,5 @@
1
- # encoding: UTF-8
2
- # Version for this gem
1
+ # frozen_string_literal: true
2
+
3
3
  module MockServer
4
- VERSION = '1.0.8.pre'
4
+ VERSION = '6.0.0'
5
5
  end
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'logger'
6
+ require 'openssl'
7
+ require 'timeout'
8
+
9
+ module MockServer
10
+ WEB_SOCKET_CORRELATION_ID_HEADER_NAME = 'WebSocketCorrelationId'
11
+ CLIENT_REGISTRATION_ID_HEADER = 'X-CLIENT-REGISTRATION-ID'
12
+
13
+ WEBSOCKET_PATH = '/_mockserver_callback_websocket'
14
+
15
+ TYPE_HTTP_REQUEST = 'org.mockserver.model.HttpRequest'
16
+ TYPE_HTTP_RESPONSE = 'org.mockserver.model.HttpResponse'
17
+ TYPE_HTTP_REQUEST_AND_RESPONSE = 'org.mockserver.model.HttpRequestAndHttpResponse'
18
+ TYPE_CLIENT_ID_DTO = 'org.mockserver.serialization.model.WebSocketClientIdDTO'
19
+ TYPE_ERROR_DTO = 'org.mockserver.serialization.model.WebSocketErrorDTO'
20
+
21
+ MAX_RECONNECT_ATTEMPTS = 3
22
+ REGISTRATION_TIMEOUT = 10
23
+
24
+ def self.extract_correlation_id(request)
25
+ return nil if request.headers.nil?
26
+
27
+ request.headers.each do |header|
28
+ if header.name == WEB_SOCKET_CORRELATION_ID_HEADER_NAME
29
+ return header.values.first if header.values && !header.values.empty?
30
+ end
31
+ end
32
+ nil
33
+ end
34
+
35
+ def self.add_correlation_id_header(message, correlation_id)
36
+ message.headers ||= []
37
+ message.headers.each do |header|
38
+ if header.name == WEB_SOCKET_CORRELATION_ID_HEADER_NAME
39
+ header.values = [correlation_id]
40
+ return message
41
+ end
42
+ end
43
+ message.headers << KeyToMultiValue.new(
44
+ name: WEB_SOCKET_CORRELATION_ID_HEADER_NAME,
45
+ values: [correlation_id]
46
+ )
47
+ message
48
+ end
49
+
50
+ def self.build_ws_message(type_name, value_dict)
51
+ JSON.generate({
52
+ 'type' => type_name,
53
+ 'value' => JSON.generate(value_dict)
54
+ })
55
+ end
56
+
57
+ def self.build_error_message(error_msg, correlation_id)
58
+ JSON.generate({
59
+ 'type' => TYPE_ERROR_DTO,
60
+ 'value' => JSON.generate({
61
+ 'message' => error_msg,
62
+ 'webSocketCorrelationId' => correlation_id
63
+ })
64
+ })
65
+ end
66
+
67
+ # @api private
68
+ def self.clean_context_path(context_path)
69
+ return '' if context_path.nil? || context_path.empty?
70
+ return "/#{context_path}" unless context_path.start_with?('/')
71
+
72
+ context_path
73
+ end
74
+
75
+ class WebSocketClient
76
+ attr_reader :client_id
77
+
78
+ def initialize
79
+ @ws = nil
80
+ @client_id = nil
81
+ @response_callback = nil
82
+ @forward_callback = nil
83
+ @forward_response_callback = nil
84
+ @stopped = false
85
+ @connected = false
86
+ @listen_thread = nil
87
+ @host = ''
88
+ @port = 0
89
+ @context_path = ''
90
+ @secure = false
91
+ @ca_cert_path = nil
92
+ @tls_verify = true
93
+ @logger = Logger.new($stdout)
94
+ @logger.progname = 'MockServer::WebSocketClient'
95
+ @logger.level = Logger::WARN
96
+ @registration_queue = nil
97
+ end
98
+
99
+ def connected?
100
+ @connected && !@stopped
101
+ end
102
+
103
+ def connect(host, port, context_path: '', secure: false,
104
+ ca_cert_path: nil, client_id: nil, tls_verify: true)
105
+ @host = host
106
+ @port = port
107
+ @context_path = context_path
108
+ @secure = secure
109
+ @ca_cert_path = ca_cert_path
110
+ @tls_verify = tls_verify
111
+
112
+ registration_id = client_id || SecureRandom.uuid
113
+
114
+ do_connect(registration_id)
115
+ @client_id
116
+ end
117
+
118
+ def register_response_callback(callback_fn)
119
+ @response_callback = callback_fn
120
+ end
121
+
122
+ def register_forward_callback(forward_fn, response_fn = nil)
123
+ @forward_callback = forward_fn
124
+ @forward_response_callback = response_fn
125
+ end
126
+
127
+ def listen
128
+ @listen_thread = Thread.new { listen_loop }
129
+ @listen_thread.abort_on_exception = false
130
+ end
131
+
132
+ def close
133
+ @stopped = true
134
+ @connected = false
135
+ @ws&.close if @ws
136
+ rescue StandardError
137
+ # ignore close errors
138
+ ensure
139
+ @ws = nil
140
+ @listen_thread&.join(5) if @listen_thread && @listen_thread != Thread.current
141
+ end
142
+
143
+ private
144
+
145
+ def do_connect(registration_id)
146
+ require 'websocket-client-simple'
147
+
148
+ scheme = @secure ? 'wss' : 'ws'
149
+ path = MockServer.clean_context_path(@context_path) + WEBSOCKET_PATH
150
+ uri = "#{scheme}://#{@host}:#{@port}#{path}"
151
+
152
+ headers = { CLIENT_REGISTRATION_ID_HEADER => registration_id }
153
+
154
+ @registration_queue = Queue.new
155
+ @connected = false
156
+ ws_client = self
157
+
158
+ opts = { headers: headers }
159
+
160
+ if @secure
161
+ if @ca_cert_path
162
+ cert_store = OpenSSL::X509::Store.new
163
+ cert_store.set_default_paths
164
+ cert_store.add_file(@ca_cert_path)
165
+ opts[:cert_store] = cert_store
166
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
167
+ elsif !@tls_verify
168
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
169
+ else
170
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_PEER
171
+ end
172
+ end
173
+
174
+ @ws = ::WebSocket::Client::Simple.connect(uri, **opts) do |ws|
175
+ ws.on :message do |msg|
176
+ ws_client.send(:handle_raw_message, msg.data)
177
+ end
178
+ ws.on :error do |e|
179
+ ws_client.send(:handle_ws_error, e)
180
+ end
181
+ ws.on :close do |_e|
182
+ ws_client.send(:handle_ws_close)
183
+ end
184
+ ws.on :open do
185
+ ws_client.send(:handle_ws_open)
186
+ end
187
+ end
188
+
189
+ result = nil
190
+ begin
191
+ Timeout.timeout(REGISTRATION_TIMEOUT) do
192
+ result = @registration_queue.pop
193
+ end
194
+ rescue Timeout::Error
195
+ @ws&.close rescue nil
196
+ @ws = nil
197
+ @connected = false
198
+ raise WebSocketError,
199
+ "WebSocket registration timed out after #{REGISTRATION_TIMEOUT}s " \
200
+ "connecting to #{@host}:#{@port}"
201
+ end
202
+
203
+ if result.is_a?(Exception)
204
+ raise result
205
+ end
206
+
207
+ @client_id = result
208
+ end
209
+
210
+ def handle_ws_open
211
+ @connected = true
212
+ end
213
+
214
+ def handle_ws_close
215
+ @connected = false
216
+ end
217
+
218
+ def handle_ws_error(error)
219
+ @connected = false
220
+ if @registration_queue
221
+ ws_error = WebSocketError.new(
222
+ "WebSocket connection error to #{@host}:#{@port}: #{error.message}"
223
+ )
224
+ @registration_queue.push(ws_error)
225
+ end
226
+ end
227
+
228
+ def listen_loop
229
+ reconnect_attempts = 0
230
+ until @stopped
231
+ sleep 0.1
232
+ next if connected?
233
+
234
+ break if @stopped
235
+
236
+ reconnect_attempts += 1
237
+ if reconnect_attempts > MAX_RECONNECT_ATTEMPTS
238
+ @logger.error('Max reconnect attempts reached, giving up')
239
+ break
240
+ end
241
+
242
+ @logger.warn("WebSocket disconnected, reconnecting (attempt #{reconnect_attempts}/#{MAX_RECONNECT_ATTEMPTS})")
243
+ begin
244
+ do_connect(@client_id || SecureRandom.uuid)
245
+ reconnect_attempts = 0
246
+ rescue StandardError => e
247
+ @logger.error("Reconnection failed: #{e.message}")
248
+ sleep [2**reconnect_attempts, 8].min
249
+ end
250
+ end
251
+ end
252
+
253
+ def handle_raw_message(raw_message)
254
+ begin
255
+ parsed = JSON.parse(raw_message)
256
+ rescue JSON::ParserError => e
257
+ @logger.warn("Received unparseable WebSocket message: #{e.message}")
258
+ return
259
+ end
260
+ msg_type = parsed['type']
261
+ msg_value = parsed['value']
262
+
263
+ if msg_type == TYPE_CLIENT_ID_DTO
264
+ value = JSON.parse(msg_value)
265
+ @client_id = value['clientId']
266
+ @registration_queue&.push(@client_id)
267
+ return
268
+ end
269
+
270
+ if msg_type == TYPE_HTTP_REQUEST
271
+ request = HttpRequest.from_hash(JSON.parse(msg_value))
272
+ correlation_id = MockServer.extract_correlation_id(request)
273
+
274
+ if @forward_callback
275
+ handle_forward_request(request, correlation_id)
276
+ elsif @response_callback
277
+ handle_response_request(request, correlation_id)
278
+ else
279
+ @logger.warn("Received HttpRequest callback but no callback registered")
280
+ end
281
+ return
282
+ end
283
+
284
+ if msg_type == TYPE_HTTP_REQUEST_AND_RESPONSE
285
+ req_and_resp = HttpRequestAndHttpResponse.from_hash(JSON.parse(msg_value))
286
+ correlation_id = MockServer.extract_correlation_id(req_and_resp.http_request)
287
+
288
+ if @forward_response_callback
289
+ handle_forward_response(req_and_resp, correlation_id)
290
+ else
291
+ @logger.warn("Received HttpRequestAndHttpResponse callback but no forward_response_callback registered")
292
+ end
293
+ return
294
+ end
295
+
296
+ @logger.warn("Received unhandled WebSocket message type: #{msg_type}")
297
+ end
298
+
299
+ def handle_response_request(request, correlation_id)
300
+ result = @response_callback.call(request)
301
+ unless result.is_a?(HttpResponse)
302
+ raise CallbackError, "Response callback must return HttpResponse, got #{result.class}"
303
+ end
304
+
305
+ MockServer.add_correlation_id_header(result, correlation_id) if correlation_id
306
+ msg = MockServer.build_ws_message(TYPE_HTTP_RESPONSE, result.to_h)
307
+ @ws.send(msg)
308
+ rescue StandardError => exc
309
+ @logger.error("Error in response callback: #{exc.message}")
310
+ if correlation_id
311
+ error_msg = MockServer.build_error_message(exc.message, correlation_id)
312
+ @ws.send(error_msg)
313
+ end
314
+ end
315
+
316
+ def handle_forward_request(request, correlation_id)
317
+ result = @forward_callback.call(request)
318
+ unless result.is_a?(HttpRequest)
319
+ raise CallbackError, "Forward callback must return HttpRequest, got #{result.class}"
320
+ end
321
+
322
+ MockServer.add_correlation_id_header(result, correlation_id) if correlation_id
323
+ msg = MockServer.build_ws_message(TYPE_HTTP_REQUEST, result.to_h)
324
+ @ws.send(msg)
325
+ rescue StandardError => exc
326
+ @logger.error("Error in forward callback: #{exc.message}")
327
+ if correlation_id
328
+ error_msg = MockServer.build_error_message(exc.message, correlation_id)
329
+ @ws.send(error_msg)
330
+ end
331
+ end
332
+
333
+ def handle_forward_response(req_and_resp, correlation_id)
334
+ result = @forward_response_callback.call(
335
+ req_and_resp.http_request,
336
+ req_and_resp.http_response
337
+ )
338
+ unless result.is_a?(HttpResponse)
339
+ raise CallbackError, "Forward response callback must return HttpResponse, got #{result.class}"
340
+ end
341
+
342
+ MockServer.add_correlation_id_header(result, correlation_id) if correlation_id
343
+ msg = MockServer.build_ws_message(TYPE_HTTP_RESPONSE, result.to_h)
344
+ @ws.send(msg)
345
+ rescue StandardError => exc
346
+ @logger.error("Error in forward response callback: #{exc.message}")
347
+ if correlation_id
348
+ error_msg = MockServer.build_error_message(exc.message, correlation_id)
349
+ @ws.send(error_msg)
350
+ end
351
+ end
352
+ end
353
+ end
@@ -1,17 +1,8 @@
1
- # encoding: UTF-8
2
- require_relative './mockserver/version'
3
- require_relative './mockserver/mock_server_client'
4
- require_relative './mockserver/proxy_client'
1
+ # frozen_string_literal: true
5
2
 
6
- # Setup serialization correctly with multi_json
7
- require 'json/pure'
8
-
9
- # To fix serialization bugs. See: http://prettystatemachine.blogspot.com/2010/09/typeerrors-in-tojson-make-me-briefly.html
10
- class Fixnum
11
- def to_json(_)
12
- to_s
13
- end
14
- end
15
-
16
- require 'multi_json'
17
- MultiJson.use(:json_pure)
3
+ require_relative 'mockserver/version'
4
+ require_relative 'mockserver/errors'
5
+ require_relative 'mockserver/models'
6
+ require_relative 'mockserver/websocket_client'
7
+ require_relative 'mockserver/forward_chain_expectation'
8
+ require_relative 'mockserver/client'
@@ -1,37 +1,36 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'mockserver/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = 'mockserver-client'
8
- spec.version = MockServer::VERSION
9
- spec.authors = ['Nayyara Samuel', 'James D Bloom']
10
- spec.email = ['nayyara.samuel@opower.com', 'jamesdbloom@gmail.com']
11
- spec.homepage = 'http://www.mock-server.com'
12
- spec.summary = 'A Ruby client for MockServer'
13
- spec.description = 'A Ruby Client for MockServer that enables easy mocking of any system you integrate with via HTTP or HTTPS (i.e. services, web sites, etc)'
8
+ spec.name = 'mockserver-client'
9
+ spec.version = MockServer::VERSION
10
+ spec.authors = ['James Bloom']
11
+ spec.email = ['jamesdbloom@gmail.com']
12
+ spec.summary = 'Official Ruby client for MockServer'
13
+ spec.description = 'Official Ruby client for MockServer — create expectations, verify requests, ' \
14
+ 'and register dynamic callbacks via WebSocket. Includes a fluent builder DSL.'
15
+ spec.homepage = 'https://www.mock-server.com'
16
+ spec.license = 'Apache-2.0'
17
+
18
+ spec.required_ruby_version = '>= 3.0'
14
19
 
15
- spec.required_ruby_version = '>= 1.9'
16
- spec.required_rubygems_version = '~> 2.0'
20
+ spec.metadata = {
21
+ 'source_code_uri' => 'https://github.com/mock-server/mockserver-monorepo',
22
+ 'changelog_uri' => 'https://www.mock-server.com/mock_server/changelog.html',
23
+ 'bug_tracker_uri' => 'https://github.com/mock-server/mockserver-monorepo/issues',
24
+ 'documentation_uri' => 'https://www.mock-server.com/mock_server/getting_started.html',
25
+ }
17
26
 
18
- spec.files = `git ls-files -z`.split("\x0")
19
- spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
20
- spec.test_files = spec.files.grep(/^(test|spec|features)\//)
27
+ spec.files = Dir['lib/**/*.rb'] + %w[README.md Gemfile mockserver-client.gemspec]
21
28
  spec.require_paths = ['lib']
22
29
 
23
- spec.add_development_dependency 'bundler', '~> 1'
24
- spec.add_development_dependency 'rake', '~> 10.3'
25
- spec.add_development_dependency 'rspec', '~> 3.0'
26
- spec.add_development_dependency 'simplecov', '~> 0.8'
27
- spec.add_development_dependency 'webmock', '~> 1.18'
28
- spec.add_development_dependency 'rubocop', '~> 0.23'
30
+ spec.add_dependency 'logger', '>= 1.0'
31
+ spec.add_dependency 'websocket-client-simple', '~> 0.8'
29
32
 
30
- spec.add_dependency 'hashie', '~> 3.0'
31
- spec.add_dependency 'json', '~> 1.8'
32
- spec.add_dependency 'activesupport', '~> 4.1'
33
- spec.add_dependency 'rest-client', '~> 1.7'
34
- spec.add_dependency 'logging_factory', '~> 0.0.2'
35
- spec.add_dependency 'thor', '~> 0.19'
36
- spec.add_dependency 'colorize', '~> 0.7'
33
+ spec.add_development_dependency 'rspec', '~> 3.12'
34
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.6'
35
+ spec.add_development_dependency 'webmock', '~> 3.19'
37
36
  end