webmock 1.7.10 → 1.8.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 (50) hide show
  1. data/.travis.yml +2 -2
  2. data/CHANGELOG.md +98 -24
  3. data/Gemfile +2 -3
  4. data/README.md +45 -4
  5. data/Rakefile +2 -2
  6. data/lib/webmock.rb +3 -0
  7. data/lib/webmock/api.rb +34 -6
  8. data/lib/webmock/http_lib_adapters/curb_adapter.rb +4 -41
  9. data/lib/webmock/http_lib_adapters/em_http_request/em_http_request_1_x.rb +1 -1
  10. data/lib/webmock/http_lib_adapters/excon_adapter.rb +94 -0
  11. data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +31 -4
  12. data/lib/webmock/http_lib_adapters/net_http.rb +2 -0
  13. data/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb +4 -3
  14. data/lib/webmock/matchers/hash_including_matcher.rb +25 -0
  15. data/lib/webmock/rack_response.rb +8 -1
  16. data/lib/webmock/request_pattern.rb +108 -77
  17. data/lib/webmock/request_signature.rb +1 -0
  18. data/lib/webmock/stub_registry.rb +9 -8
  19. data/lib/webmock/version.rb +1 -1
  20. data/lib/webmock/webmock.rb +5 -2
  21. data/minitest/webmock_spec.rb +22 -2
  22. data/spec/acceptance/curb/curb_spec_helper.rb +12 -2
  23. data/spec/acceptance/em_http_request/em_http_request_spec.rb +42 -33
  24. data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +4 -2
  25. data/spec/acceptance/excon/excon_spec.rb +15 -0
  26. data/spec/acceptance/excon/excon_spec_helper.rb +37 -0
  27. data/spec/acceptance/net_http/net_http_spec.rb +7 -0
  28. data/spec/acceptance/net_http/net_http_spec_helper.rb +3 -1
  29. data/spec/acceptance/patron/patron_spec.rb +12 -3
  30. data/spec/acceptance/patron/patron_spec_helper.rb +2 -2
  31. data/spec/acceptance/shared/allowing_and_disabling_net_connect.rb +3 -3
  32. data/spec/acceptance/shared/callbacks.rb +22 -6
  33. data/spec/acceptance/shared/complex_cross_concern_behaviors.rb +21 -0
  34. data/spec/acceptance/shared/enabling_and_disabling_webmock.rb +10 -11
  35. data/spec/acceptance/shared/precedence_of_stubs.rb +1 -1
  36. data/spec/acceptance/shared/request_expectations.rb +49 -3
  37. data/spec/acceptance/shared/returning_declared_responses.rb +9 -21
  38. data/spec/acceptance/shared/stubbing_requests.rb +80 -4
  39. data/spec/acceptance/typhoeus/typhoeus_hydra_spec_helper.rb +1 -1
  40. data/spec/acceptance/webmock_shared.rb +11 -8
  41. data/spec/spec_helper.rb +3 -3
  42. data/spec/support/my_rack_app.rb +25 -1
  43. data/spec/support/webmock_server.rb +9 -6
  44. data/spec/unit/rack_response_spec.rb +18 -0
  45. data/spec/unit/request_pattern_spec.rb +205 -96
  46. data/spec/unit/request_signature_spec.rb +36 -34
  47. data/spec/unit/util/uri_spec.rb +14 -2
  48. data/test/shared_test.rb +31 -2
  49. data/webmock.gemspec +9 -7
  50. metadata +86 -73
@@ -189,7 +189,7 @@ if defined?(EventMachine::HttpClient)
189
189
  response_string = []
190
190
  response_string << "HTTP/1.1 #{status[0]} #{status[1]}"
191
191
 
192
- headers["Content-Length"] = body.length unless headers["Content-Length"]
192
+ headers["Content-Length"] = body.bytesize unless headers["Content-Length"]
193
193
  headers.each do |header, value|
194
194
  value = value.join(", ") if value.is_a?(Array)
195
195
 
@@ -0,0 +1,94 @@
1
+ begin
2
+ require 'excon'
3
+ rescue LoadError
4
+ # excon not found
5
+ end
6
+
7
+ if defined?(Excon)
8
+
9
+ module WebMock
10
+ module HttpLibAdapters
11
+
12
+ class ExconAdapter < HttpLibAdapter
13
+ adapter_for :excon
14
+
15
+ def self.enable!
16
+ Excon.send(:remove_const, :Connection)
17
+ Excon.send(:const_set, :Connection, ExconConnection)
18
+ end
19
+
20
+ def self.disable!
21
+ Excon.send(:remove_const, :Connection)
22
+ Excon.send(:const_set, :Connection, ExconConnection.superclass)
23
+ end
24
+
25
+
26
+ def self.to_query(hash)
27
+ string = ""
28
+ for key, values in hash
29
+ if values.nil?
30
+ string << key.to_s << '&'
31
+ else
32
+ for value in [*values]
33
+ string << key.to_s << '=' << CGI.escape(value.to_s) << '&'
34
+ end
35
+ end
36
+ end
37
+ string.chop! # remove trailing '&'
38
+ end
39
+
40
+ def self.build_request(params)
41
+ params = params.dup
42
+ method = (params.delete(:method) || :get).to_s.downcase.to_sym
43
+ params[:query] = to_query(params[:query]) if params[:query].is_a?(Hash)
44
+ uri = Addressable::URI.new(params).to_s
45
+ WebMock::RequestSignature.new method, uri, :body => params[:body], :headers => params[:headers]
46
+ end
47
+
48
+ def self.real_response(mock)
49
+ raise Excon::Errors::Timeout if mock.should_timeout
50
+ mock.raise_error_if_any
51
+ Excon::Response.new \
52
+ :body => mock.body,
53
+ :status => mock.status[0].to_i,
54
+ :headers => mock.headers
55
+ end
56
+
57
+ def self.mock_response(real)
58
+ mock = WebMock::Response.new
59
+ mock.status = real.status
60
+ mock.headers = real.headers
61
+ mock.body = real.body
62
+ mock
63
+ end
64
+
65
+ def self.perform_callbacks(request, response, options = {})
66
+ return unless WebMock::CallbackRegistry.any_callbacks?
67
+ WebMock::CallbackRegistry.invoke_callbacks(options.merge(:lib => :excon), request, response)
68
+ end
69
+
70
+ end
71
+
72
+ class ExconConnection < ::Excon::Connection
73
+
74
+ def request_kernel(params, &block)
75
+ mock_request = ExconAdapter.build_request params.dup
76
+ WebMock::RequestRegistry.instance.requested_signatures.put(mock_request)
77
+
78
+ if mock_response = WebMock::StubRegistry.instance.response_for_request(mock_request)
79
+ ExconAdapter.perform_callbacks(mock_request, mock_response, :real_request => false)
80
+ ExconAdapter.real_response(mock_response)
81
+ elsif WebMock.net_connect_allowed?(mock_request.uri)
82
+ real_response = super
83
+ ExconAdapter.perform_callbacks(mock_request, ExconAdapter.mock_response(real_response), :real_request => true)
84
+ real_response
85
+ else
86
+ raise WebMock::NetConnectNotAllowedError.new(mock_request)
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+
94
+ end
@@ -38,7 +38,7 @@ if defined?(::HTTPClient)
38
38
  end
39
39
 
40
40
  def do_get_with_webmock(req, proxy, conn, stream = false, &block)
41
- request_signature = build_request_signature(req)
41
+ request_signature = build_request_signature(req, :reuse_existing)
42
42
 
43
43
  WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
44
44
 
@@ -75,6 +75,7 @@ if defined?(::HTTPClient)
75
75
  def do_request_async_with_webmock(method, uri, query, body, extheader)
76
76
  req = create_request(method, uri, query, body, extheader)
77
77
  request_signature = build_request_signature(req)
78
+ webmock_request_signatures << request_signature
78
79
 
79
80
  if webmock_responses[request_signature] || WebMock.net_connect_allowed?(request_signature.uri)
80
81
  do_request_async_without_webmock(method, uri, query, body, extheader)
@@ -111,7 +112,17 @@ if defined?(::HTTPClient)
111
112
  def build_webmock_response(httpclient_response)
112
113
  webmock_response = WebMock::Response.new
113
114
  webmock_response.status = [httpclient_response.status, httpclient_response.reason]
114
- webmock_response.headers = httpclient_response.header.all
115
+
116
+ webmock_response.headers = {}.tap do |hash|
117
+ httpclient_response.header.all.each do |(key, value)|
118
+ if hash.has_key?(key)
119
+ hash[key] = Array(hash[key]) + [value]
120
+ else
121
+ hash[key] = value
122
+ end
123
+ end
124
+ end
125
+
115
126
  if httpclient_response.content.respond_to?(:read)
116
127
  webmock_response.body = httpclient_response.content.read
117
128
  body = HTTP::Message::Body.new
@@ -123,7 +134,7 @@ if defined?(::HTTPClient)
123
134
  webmock_response
124
135
  end
125
136
 
126
- def build_request_signature(req)
137
+ def build_request_signature(req, reuse_existing = false)
127
138
  uri = WebMock::Util::URI.heuristic_parse(req.header.request_uri.to_s)
128
139
  uri.query_values = req.header.request_query if req.header.request_query
129
140
  uri.port = req.header.request_uri.port
@@ -149,12 +160,19 @@ if defined?(::HTTPClient)
149
160
  uri.userinfo = userinfo
150
161
  end
151
162
 
152
- WebMock::RequestSignature.new(
163
+ signature = WebMock::RequestSignature.new(
153
164
  req.header.request_method.downcase.to_sym,
154
165
  uri.to_s,
155
166
  :body => req.content,
156
167
  :headers => headers
157
168
  )
169
+
170
+ # reuse a previous identical signature object if we stored one for later use
171
+ if reuse_existing && previous_signature = previous_signature_for(signature)
172
+ return previous_signature
173
+ end
174
+
175
+ signature
158
176
  end
159
177
 
160
178
  def webmock_responses
@@ -163,4 +181,13 @@ if defined?(::HTTPClient)
163
181
  end
164
182
  end
165
183
 
184
+ def webmock_request_signatures
185
+ @webmock_request_signatures ||= []
186
+ end
187
+
188
+ def previous_signature_for(signature)
189
+ return nil unless index = webmock_request_signatures.index(signature)
190
+ webmock_request_signatures.delete_at(index)
191
+ end
192
+
166
193
  end
@@ -184,6 +184,8 @@ end
184
184
 
185
185
  class StubSocket #:nodoc:
186
186
 
187
+ attr_accessor :read_timeout
188
+
187
189
  def initialize(*args)
188
190
  end
189
191
 
@@ -61,6 +61,9 @@ if defined?(Typhoeus)
61
61
  :body => body,
62
62
  :headers => req.headers
63
63
  )
64
+
65
+ req.instance_variable_set(:@__webmock_request_signature, request_signature)
66
+
64
67
  request_signature
65
68
  end
66
69
 
@@ -115,9 +118,7 @@ if defined?(Typhoeus)
115
118
  end
116
119
 
117
120
  AFTER_REQUEST_CALLBACK = Proc.new do |request|
118
- request_signature =
119
- ::WebMock::HttpLibAdapters::TyphoeusAdapter.
120
- build_request_signature(request)
121
+ request_signature = request.instance_variable_get(:@__webmock_request_signature)
121
122
  webmock_response =
122
123
  ::WebMock::HttpLibAdapters::TyphoeusAdapter.
123
124
  build_webmock_response(request.response)
@@ -0,0 +1,25 @@
1
+ module WebMock
2
+ module Matchers
3
+ #this is a based on RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher
4
+ #https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
5
+ class HashIncludingMatcher
6
+ def initialize(expected)
7
+ @expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected).sort]
8
+ end
9
+
10
+ def ==(actual)
11
+ @expected.all? {|k,v| actual.has_key?(k) && v == actual[k]}
12
+ rescue NoMethodError
13
+ false
14
+ end
15
+
16
+ def inspect
17
+ "hash_including(#{@expected.inspect})"
18
+ end
19
+
20
+ def self.from_rspec_matcher(matcher)
21
+ new(matcher.instance_variable_get(:@expected))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -10,12 +10,19 @@ module WebMock
10
10
  status, headers, response = @app.call(env)
11
11
 
12
12
  Response.new(
13
- :body => response.join,
13
+ :body => body_from_rack_response(response),
14
14
  :headers => headers,
15
15
  :status => status
16
16
  )
17
17
  end
18
18
 
19
+ def body_from_rack_response(response)
20
+ body = ""
21
+ response.each { |line| body << line }
22
+ response.close if response.respond_to?(:close)
23
+ return body
24
+ end
25
+
19
26
  def build_rack_env(request)
20
27
  uri = request.uri
21
28
  headers = request.headers || {}
@@ -40,19 +40,19 @@ module WebMock
40
40
  private
41
41
 
42
42
 
43
- def assign_options(options)
44
- @body_pattern = BodyPattern.new(options[:body]) if options.has_key?(:body)
45
- @headers_pattern = HeadersPattern.new(options[:headers]) if options.has_key?(:headers)
46
- @uri_pattern.add_query_params(options[:query]) if options.has_key?(:query)
47
- end
43
+ def assign_options(options)
44
+ @body_pattern = BodyPattern.new(options[:body]) if options.has_key?(:body)
45
+ @headers_pattern = HeadersPattern.new(options[:headers]) if options.has_key?(:headers)
46
+ @uri_pattern.add_query_params(options[:query]) if options.has_key?(:query)
47
+ end
48
48
 
49
- def create_uri_pattern(uri)
50
- if uri.is_a?(Regexp)
51
- URIRegexpPattern.new(uri)
52
- else
53
- URIStringPattern.new(uri)
49
+ def create_uri_pattern(uri)
50
+ if uri.is_a?(Regexp)
51
+ URIRegexpPattern.new(uri)
52
+ else
53
+ URIStringPattern.new(uri)
54
+ end
54
55
  end
55
- end
56
56
 
57
57
  end
58
58
 
@@ -76,9 +76,31 @@ module WebMock
76
76
  def initialize(pattern)
77
77
  @pattern = pattern.is_a?(Addressable::URI) ? pattern : WebMock::Util::URI.normalize_uri(pattern)
78
78
  end
79
+
80
+ def add_query_params(query_params)
81
+ @query_params = if query_params.is_a?(Hash)
82
+ query_params
83
+ elsif query_params.is_a?(WebMock::Matchers::HashIncludingMatcher)
84
+ query_params
85
+ elsif defined?(RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher) && query_params.is_a?(RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher)
86
+ WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(query_params)
87
+ else
88
+ Addressable::URI.parse('?' + query_params).query_values
89
+ end
90
+ end
91
+
92
+ def to_s
93
+ str = @pattern.inspect
94
+ str += " with query params #{@query_params.inspect}" if @query_params
95
+ str
96
+ end
79
97
  end
80
98
 
81
99
  class URIRegexpPattern < URIPattern
100
+ def initialize *args, &block
101
+ @query_params = nil
102
+ super
103
+ end
82
104
 
83
105
  def matches?(uri)
84
106
  WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) } &&
@@ -90,33 +112,34 @@ module WebMock
90
112
  str += " with query params #{@query_params.inspect}" if @query_params
91
113
  str
92
114
  end
93
-
94
- def add_query_params(query_params)
95
- @query_params = query_params.is_a?(Hash) ? query_params : Addressable::URI.parse('?' + query_params).query_values
96
- end
97
-
98
115
  end
99
116
 
100
117
  class URIStringPattern < URIPattern
101
118
  def matches?(uri)
102
119
  if @pattern.is_a?(Addressable::URI)
103
- uri === @pattern
120
+ if @query_params
121
+ uri.omit(:query) === @pattern && (@query_params.nil? || @query_params == uri.query_values)
122
+ else
123
+ uri === @pattern
124
+ end
104
125
  else
105
126
  false
106
127
  end
107
128
  end
108
129
 
109
130
  def add_query_params(query_params)
110
- if !query_params.is_a?(Hash)
111
- query_params = Addressable::URI.parse('?' + query_params).query_values
131
+ super
132
+ if @query_params.is_a?(Hash) || @query_params.is_a?(String)
133
+ @pattern.query_values = (@pattern.query_values || {}).merge(@query_params)
134
+ @query_params = nil
112
135
  end
113
- @pattern.query_values = (@pattern.query_values || {}).merge(query_params)
114
136
  end
115
137
 
116
138
  def to_s
117
- WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
139
+ str = WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
140
+ str += " with query params #{@query_params.inspect}" if @query_params
141
+ str
118
142
  end
119
-
120
143
  end
121
144
 
122
145
 
@@ -136,24 +159,21 @@ module WebMock
136
159
  }
137
160
 
138
161
  def initialize(pattern)
139
- @pattern = pattern
140
- if (@pattern).is_a?(Hash)
141
- @pattern = normalize_hash(@pattern)
162
+ @pattern = if pattern.is_a?(Hash)
163
+ normalize_hash(pattern)
164
+ elsif defined?(RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher) && pattern.is_a?(RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher)
165
+ WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(pattern)
166
+ else
167
+ pattern
142
168
  end
143
169
  end
144
170
 
145
171
  def matches?(body, content_type = "")
146
172
  if (@pattern).is_a?(Hash)
147
173
  return true if @pattern.empty?
148
-
149
- case BODY_FORMATS[content_type]
150
- when :json then
151
- matching_hashes?(WebMock::Util::JSON.parse(body), @pattern)
152
- when :xml then
153
- matching_hashes?(Crack::XML.parse(body), @pattern)
154
- else
155
- matching_hashes?(Addressable::URI.parse('?' + body).query_values, @pattern)
156
- end
174
+ matching_hashes?(body_as_hash(body, content_type), @pattern)
175
+ elsif (@pattern).is_a?(WebMock::Matchers::HashIncludingMatcher)
176
+ @pattern == body_as_hash(body, content_type)
157
177
  else
158
178
  empty_string?(@pattern) && empty_string?(body) ||
159
179
  @pattern == body ||
@@ -166,51 +186,62 @@ module WebMock
166
186
  end
167
187
 
168
188
  private
169
-
170
- # Compare two hashes for equality
171
- #
172
- # For two hashes to match they must have the same length and all
173
- # values must match when compared using `#===`.
174
- #
175
- # The following hashes are examples of matches:
176
- #
177
- # {a: /\d+/} and {a: '123'}
178
- #
179
- # {a: '123'} and {a: '123'}
180
- #
181
- # {a: {b: /\d+/}} and {a: {b: '123'}}
182
- #
183
- # {a: {b: 'wow'}} and {a: {b: 'wow'}}
184
- #
185
- # @param [Hash] query_parameters typically the result of parsing
186
- # JSON, XML or URL encoded parameters.
187
- #
188
- # @param [Hash] pattern which contains keys with a string, hash or
189
- # regular expression value to use for comparison.
190
- #
191
- # @return [Boolean] true if the paramaters match the comparison
192
- # hash, false if not.
193
- def matching_hashes?(query_parameters, pattern)
194
- return false unless query_parameters.size == pattern.size
195
- query_parameters.each do |key, actual|
196
- expected = pattern[key]
197
-
198
- if actual.is_a?(Hash) && expected.is_a?(Hash)
199
- return false unless matching_hashes?(actual, expected)
189
+ def body_as_hash(body, content_type)
190
+ case BODY_FORMATS[content_type]
191
+ when :json then
192
+ WebMock::Util::JSON.parse(body)
193
+ when :xml then
194
+ Crack::XML.parse(body)
200
195
  else
201
- return false unless expected === actual
196
+ Addressable::URI.parse('?' + body).query_values
202
197
  end
203
198
  end
204
- true
205
- end
206
199
 
207
- def empty_string?(string)
208
- string.nil? || string == ""
209
- end
200
+ # Compare two hashes for equality
201
+ #
202
+ # For two hashes to match they must have the same length and all
203
+ # values must match when compared using `#===`.
204
+ #
205
+ # The following hashes are examples of matches:
206
+ #
207
+ # {a: /\d+/} and {a: '123'}
208
+ #
209
+ # {a: '123'} and {a: '123'}
210
+ #
211
+ # {a: {b: /\d+/}} and {a: {b: '123'}}
212
+ #
213
+ # {a: {b: 'wow'}} and {a: {b: 'wow'}}
214
+ #
215
+ # @param [Hash] query_parameters typically the result of parsing
216
+ # JSON, XML or URL encoded parameters.
217
+ #
218
+ # @param [Hash] pattern which contains keys with a string, hash or
219
+ # regular expression value to use for comparison.
220
+ #
221
+ # @return [Boolean] true if the paramaters match the comparison
222
+ # hash, false if not.
223
+ def matching_hashes?(query_parameters, pattern)
224
+ return false unless query_parameters.is_a?(Hash)
225
+ return false unless query_parameters.keys.sort == pattern.keys.sort
226
+ query_parameters.each do |key, actual|
227
+ expected = pattern[key]
228
+
229
+ if actual.is_a?(Hash) && expected.is_a?(Hash)
230
+ return false unless matching_hashes?(actual, expected)
231
+ else
232
+ return false unless expected === actual
233
+ end
234
+ end
235
+ true
236
+ end
210
237
 
211
- def normalize_hash(hash)
212
- WebMock::Util::HashKeysStringifier.stringify_keys!(hash)
213
- end
238
+ def empty_string?(string)
239
+ string.nil? || string == ""
240
+ end
241
+
242
+ def normalize_hash(hash)
243
+ Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash).sort]
244
+ end
214
245
 
215
246
  end
216
247
 
@@ -237,9 +268,9 @@ module WebMock
237
268
 
238
269
  private
239
270
 
240
- def empty_headers?(headers)
241
- headers.nil? || headers == {}
242
- end
271
+ def empty_headers?(headers)
272
+ headers.nil? || headers == {}
273
+ end
243
274
  end
244
275
 
245
276
  end