webmock 1.7.10 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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