request_interceptor 0.3.0 → 1.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
  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