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 +4 -4
- data/README.md +72 -0
- data/lib/request_interceptor.rb +20 -19
- data/lib/request_interceptor/matchers.rb +159 -0
- data/lib/request_interceptor/transaction.rb +83 -0
- data/lib/request_interceptor/version.rb +1 -1
- data/request_interceptor.gemspec +2 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c6e50636bc572fbc38345fc4b43857d64190bf8
|
4
|
+
data.tar.gz: 05df14491c1257858527465fbad1aca49dddb04f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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].
|
data/lib/request_interceptor.rb
CHANGED
@@ -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
|
-
|
79
|
-
|
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
|
+
|
data/request_interceptor.gemspec
CHANGED
@@ -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.
|
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-
|
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
|