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 +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
|