request_interceptor 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 15e0184221194d0cd9222006c1739e23f7c81a89
4
- data.tar.gz: 5bb90d1e9b0b9b4d0eee2cadb6015e324db86e42
3
+ metadata.gz: 0c6e50636bc572fbc38345fc4b43857d64190bf8
4
+ data.tar.gz: 05df14491c1257858527465fbad1aca49dddb04f
5
5
  SHA512:
6
- metadata.gz: e36776a7b15b8994100a480d5ebda2d12373d09b54d9cd48c22c2488b2623d3143b19f078f1bd067a8a90956b1d3be853388f5875616ffcbc8d8061e4c0576a2
7
- data.tar.gz: 74d7a949efa97b601324b38ef7666741294ee5a00e6bad56d328d2e53f923a79cd3d3bdb0801bf66ec1242b0129b58ad415f0a589cccc227ccd9a3afa4e67233
6
+ metadata.gz: 1a4382a22cba58d5554447300619e3d84ea6d980ebc87a6cc2f95842f912b06f8a25f62175104bff61f4efbe47c91d5a3c8ba89d8ef4b92a0e6d33cbd4a502b6
7
+ data.tar.gz: 65cbed11f12a44a98d8b4ccca0085e6092c2ab5858e486c19df1772c5b546bddd4e9948311a3f08280f3eaf40d729c36cbe9a9eb7c368cfb141e60781e385701
data/README.md CHANGED
@@ -121,6 +121,78 @@ multilingual_app.intercept do
121
121
  end
122
122
  ```
123
123
 
124
+ ### RSpec Integration
125
+
126
+ Request Interceptor has built in support for RSpec.
127
+ The matcher that ships with the gem supports matching
128
+
129
+ * the request method,
130
+ * the path
131
+ * the query parameters and
132
+ * the request body.
133
+
134
+ Unless otherwise specified, the matcher uses RSpec's own equality matcher for all comparisons:
135
+
136
+ ```ruby
137
+ hello_world_app = RequestInterceptor.define do
138
+ host "example.com"
139
+
140
+ get "/" do
141
+ # ...
142
+ end
143
+
144
+ post "/articles" do
145
+ # ...
146
+ end
147
+ end
148
+
149
+ log = hello_world_app.intercept do
150
+ Net::HTTP.get(URI("http://example.com/"))
151
+ end
152
+
153
+ expect(log).to contain_intercepted_request(:get, "/")
154
+ ```
155
+
156
+ The example above only succeeds if the path is exactly `"/"`.
157
+ While this is generally desired for matching the path or the request method, it can be too restrictive when matching against the query or the request body.
158
+ The example below demonstrates how to use `with_body` in conjunction with RSpec's own `including` matcher to match against a subset of the request body.
159
+
160
+ ```ruby
161
+ log = hello_world_app.intercept do
162
+ uri = URI("http://example.com/")
163
+ client = Net::HTTP.new(uri.host, uri.port)
164
+ request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
165
+ request.body = "{title: \"Hello World!\", content: \"Some irrelevant content.\"}"
166
+ client.request(request)
167
+ end
168
+
169
+ expect(log).to contain_intercepted_request(:post, "/articles").with_body(including(title: "Hello World!"))
170
+ ```
171
+
172
+ As the example above indicates, Request Interceptor automatically parses JSON request bodies to make matching easier.
173
+
174
+ Similar to `with_body`, the RSpec matcher also provides a `with_query` method, to match against query parameters:
175
+
176
+ ```ruby
177
+ log = hello_world_app.intercept do
178
+ Net::HTTP.get(URI("http://example.com/?q=hello+world"))
179
+ end
180
+
181
+ expect(log).to contain_intercepted_request(:get, "/").with_query(q: "hello+world")
182
+ ```
183
+
184
+ Lastly, `count` can be used to specify the number of times a particular request is to be expected.
185
+ It takes an integer or a range as its argument.
186
+
187
+ ```ruby
188
+ log = hello_world_app.intercept do
189
+ Net::HTTP.get(URI("http://example.com/"))
190
+ Net::HTTP.get(URI("http://example.com/"))
191
+ end
192
+
193
+ expect(log).to contain_intercepted_request(:get, "/").count(2)
194
+ ```
195
+
124
196
  ## Contributing
125
197
 
126
198
  Bug reports and pull requests are welcome on GitHub at (t6d/request_interceptor)[https://github.com/t6d/request_interceptor].
@@ -1,25 +1,13 @@
1
1
  require "request_interceptor/version"
2
2
 
3
+ require "active_support/core_ext/hash"
4
+ require "active_support/json"
5
+ require "rack"
6
+ require "smart_properties"
3
7
  require "webmock"
4
- require "uri"
5
8
 
6
- class RequestInterceptor
7
- Transaction = Struct.new(:request, :response)
8
-
9
- module RequestBackwardsCompatibility
10
- def path
11
- uri.request_uri
12
- end
13
-
14
- def uri
15
- URI.parse(super.to_s)
16
- end
17
-
18
- def method
19
- super.to_s.upcase
20
- end
21
- end
22
9
 
10
+ class RequestInterceptor
23
11
  class ApplicationWrapper < SimpleDelegator
24
12
  attr_reader :pattern
25
13
 
@@ -75,8 +63,19 @@ class RequestInterceptor
75
63
 
76
64
  request_logging = ->(request, response) do
77
65
  next unless applications.any? { |application| application.intercepts?(request.uri) }
78
- request.extend(RequestBackwardsCompatibility)
79
- transactions << Transaction.new(request, response)
66
+ transactions << Transaction.new(
67
+ request: Transaction::Request.new(
68
+ method: request.method,
69
+ uri: URI(request.uri.to_s),
70
+ headers: request.headers,
71
+ body: request.body
72
+ ),
73
+ response: Transaction::Response.new(
74
+ status_code: response.status,
75
+ headers: response.headers,
76
+ body: response.body
77
+ )
78
+ )
80
79
  end
81
80
 
82
81
  WebMockManager.new(applications, request_logging).run_simulation(&simulation)
@@ -86,6 +85,8 @@ class RequestInterceptor
86
85
  end
87
86
 
88
87
  require_relative "request_interceptor/application"
88
+ require_relative "request_interceptor/matchers" if defined? RSpec
89
+ require_relative "request_interceptor/transaction"
89
90
  require_relative "request_interceptor/webmock_manager"
90
91
  require_relative "request_interceptor/webmock_patches"
91
92
 
@@ -0,0 +1,159 @@
1
+ module RequestInterceptor::Matchers
2
+ class MatcherWrapper < SimpleDelegator
3
+ def ===(object)
4
+ matches?(object)
5
+ end
6
+
7
+ def to_s
8
+ description
9
+ end
10
+ end
11
+
12
+ class InterceptedRequest
13
+ attr_reader :method
14
+ attr_reader :path
15
+ attr_reader :query
16
+ attr_reader :body
17
+ attr_reader :transactions
18
+
19
+ def initialize(method, path)
20
+ @method = method
21
+ @path = path
22
+ @count = (1..Float::INFINITY)
23
+ @transactions = []
24
+ end
25
+
26
+ ##
27
+ # Chains
28
+ ##
29
+
30
+ def count(count = nil)
31
+ return @count if count.nil?
32
+
33
+ @count =
34
+ case count
35
+ when Integer
36
+ (count .. count)
37
+ when Range
38
+ count
39
+ else
40
+ raise ArgumentError
41
+ end
42
+
43
+ self
44
+ end
45
+
46
+ def with_query(query)
47
+ query_matcher =
48
+ if query.respond_to?(:matches?) && query.respond_to?(:failure_message)
49
+ query
50
+ else
51
+ RSpec::Matchers::BuiltIn::Eq.new(query)
52
+ end
53
+
54
+ @query = MatcherWrapper.new(query_matcher)
55
+
56
+ self
57
+ end
58
+
59
+ def with_body(body)
60
+ body_matcher =
61
+ if body.respond_to?(:matches?) && body.respond_to?(:failure_message)
62
+ body
63
+ else
64
+ RSpec::Matchers::BuiltIn::Eq.new(body)
65
+ end
66
+
67
+ @body = MatcherWrapper.new(body_matcher)
68
+
69
+ self
70
+ end
71
+
72
+ ##
73
+ # Rspec Matcher Protocol
74
+ ##
75
+
76
+ def matches?(transactions)
77
+ @transactions = transactions
78
+ count.cover?(matching_transactions.count)
79
+ end
80
+
81
+ def failure_message
82
+ expected_request = "#{format_method(method)} #{path}"
83
+ expected_request += " with query #{format_object(query)}" if query
84
+ expected_request += " and" if query && body
85
+ expected_request += " with body #{format_object(body)}" if body
86
+
87
+ similar_intercepted_requests = similar_transactions.map.with_index do |transaction, index|
88
+ method = format_method(transaction.request.method)
89
+ path = transaction.request.path
90
+ query = transaction.request.query
91
+ body = transaction.request.body
92
+ indentation_required = index != 0
93
+
94
+ message = "#{method} #{path}"
95
+ message += (query.nil? || query.empty?) ? " with no query" : " with query #{format_object(query)}"
96
+ message += (body.nil? || body.empty?) ? " and with no body" : " and with body #{format_object(body)}"
97
+ message = " " * 10 + message if indentation_required
98
+ message
99
+ end
100
+
101
+ similar_intercepted_requests = ["none"] if similar_intercepted_requests.none?
102
+
103
+ "\nexpected: #{expected_request}" \
104
+ "\n got: #{similar_intercepted_requests.join("\n")}"
105
+ end
106
+
107
+ def failure_message_when_negated
108
+ message = "intercepted a #{format_method(method)} request to #{path}"
109
+ message += " with query #{format_object(query)}" if query
110
+ message += " and" if query && body
111
+ message += " with body #{format_object(body)}" if body
112
+ message
113
+ end
114
+
115
+ def description
116
+ "should intercept a #{format_method(method)} request to #{path}"
117
+ end
118
+
119
+ ##
120
+ # Helper methods
121
+ ##
122
+
123
+ private
124
+
125
+ def format_method(method)
126
+ method.to_s.upcase
127
+ end
128
+
129
+ def format_object(object)
130
+ RSpec::Support::ObjectFormatter.format(object)
131
+ end
132
+
133
+ def matching_transactions
134
+ transactions.select do |transaction|
135
+ request = transaction.request
136
+ request.method?(method) &&
137
+ request.path?(path) &&
138
+ request.query?(query) &&
139
+ request.body?(body)
140
+ end
141
+ end
142
+
143
+ def similar_transactions
144
+ transactions.select do |transaction|
145
+ request = transaction.request
146
+ request.method?(method) && request.path?(path)
147
+ end
148
+ end
149
+ end
150
+
151
+ def have_intercepted_request(*args)
152
+ InterceptedRequest.new(*args)
153
+ end
154
+ alias contain_intercepted_request have_intercepted_request
155
+ end
156
+
157
+ RSpec.configure do |config|
158
+ config.include(RequestInterceptor::Matchers)
159
+ end
@@ -0,0 +1,83 @@
1
+ class RequestInterceptor::Transaction
2
+ class HTTPMessage
3
+ include SmartProperties
4
+ property :body
5
+
6
+ def initialize(*args, headers: {}, **kwargs)
7
+ @headers = headers.dup
8
+ super(*args, **kwargs)
9
+ end
10
+
11
+ def [](name)
12
+ @headers[name]
13
+ end
14
+
15
+ def []=(name, value)
16
+ @headers[name] = value
17
+ end
18
+
19
+ def headers
20
+ @headers.dup
21
+ end
22
+ end
23
+
24
+ class Request < HTTPMessage
25
+ property :method, converts: ->(method) { method.to_s.upcase.freeze }, required: true
26
+ property :uri, accepts: URI, converts: ->(uri) { URI(uri.to_s) }, required: true
27
+ property :body
28
+
29
+ def method?(method)
30
+ normalized_method = method.to_s.upcase
31
+ normalized_method == self.method
32
+ end
33
+
34
+ def path?(path)
35
+ path === self.path
36
+ end
37
+
38
+ def path
39
+ uri.path
40
+ end
41
+
42
+ def request_uri?(request_uri)
43
+ request_uri === self.request_uri
44
+ end
45
+
46
+ def request_uri
47
+ uri.request_uri
48
+ end
49
+
50
+ def query
51
+ Rack::Utils.parse_nested_query(uri.query).deep_symbolize_keys!
52
+ end
53
+
54
+ def query?(query_matcher)
55
+ return true if query_matcher.nil?
56
+ query_matcher === self.query
57
+ end
58
+
59
+ def body?(body_matcher)
60
+ return true if body_matcher.nil?
61
+
62
+ body = case self["Content-Type"]
63
+ when "application/json"
64
+ ActiveSupport::JSON.decode(self.body).deep_symbolize_keys!
65
+ else
66
+ self.body
67
+ end
68
+
69
+ body_matcher === body
70
+ end
71
+ end
72
+
73
+ class Response < HTTPMessage
74
+ property :status_code, required: true
75
+ property :body
76
+ end
77
+
78
+ include SmartProperties
79
+
80
+ property :request, accepts: Request
81
+ property :response, accepts: Response
82
+ end
83
+
@@ -1,3 +1,3 @@
1
1
  class RequestInterceptor
2
- VERSION = "0.3.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -21,6 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.add_runtime_dependency "sinatra"
22
22
  spec.add_runtime_dependency "rack"
23
23
  spec.add_runtime_dependency "webmock", "~> 3.0"
24
+ spec.add_runtime_dependency "smart_properties", "~> 1.0"
25
+ spec.add_runtime_dependency "activesupport", ">= 4.0"
24
26
 
25
27
  spec.add_development_dependency "bundler", "~> 1.9"
26
28
  spec.add_development_dependency "rake", "~> 10.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: request_interceptor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Tennhard
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-05-08 00:00:00.000000000 Z
12
+ date: 2017-10-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
@@ -53,6 +53,34 @@ dependencies:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
55
  version: '3.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: smart_properties
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: activesupport
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '4.0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '4.0'
56
84
  - !ruby/object:Gem::Dependency
57
85
  name: bundler
58
86
  requirement: !ruby/object:Gem::Requirement
@@ -129,6 +157,8 @@ files:
129
157
  - circle.yml
130
158
  - lib/request_interceptor.rb
131
159
  - lib/request_interceptor/application.rb
160
+ - lib/request_interceptor/matchers.rb
161
+ - lib/request_interceptor/transaction.rb
132
162
  - lib/request_interceptor/version.rb
133
163
  - lib/request_interceptor/webmock_manager.rb
134
164
  - lib/request_interceptor/webmock_patches.rb