hyperion_http 0.1.9 → 0.2.1
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/.travis.yml +1 -1
- data/CHANGES.md +5 -1
- data/hyperion.gemspec +1 -0
- data/hyperion_http.gemspec +2 -3
- data/lib/hyperion/aux/logger.rb +1 -2
- data/lib/hyperion_test/fake.rb +17 -8
- data/lib/hyperion_test/fake_server.rb +59 -27
- data/lib/hyperion_test/fake_server/config.rb +2 -3
- data/lib/hyperion_test/fake_server/types.rb +4 -2
- data/lib/hyperion_test/kim.rb +223 -0
- data/lib/hyperion_test/kim/matcher.rb +86 -0
- data/lib/hyperion_test/kim/matchers.rb +48 -0
- data/lib/hyperion_test/test_framework_hooks.rb +6 -6
- data/spec/lib/hyperion/requestor_spec.rb +2 -3
- data/spec/lib/hyperion/test_spec.rb +17 -17
- data/spec/lib/hyperion_test/kim/matcher_spec.rb +126 -0
- data/spec/lib/hyperion_test/kim/matchers_spec.rb +58 -0
- data/spec/lib/hyperion_test/kim_spec.rb +157 -0
- data/update_version.sh +52 -0
- metadata +19 -11
- data/lib/hyperion/aux/version.rb +0 -3
- data/lib/hyperion_test/fake_server/dispatcher.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 960780555d961a067c45dd406b4a77c222d3b870
|
4
|
+
data.tar.gz: a9ccdfa4d464305cd35d4d79bc392760bd393327
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f67e72757677c9f6c10f088d9c6965436a6442ffe93dc65e42a5654ac525f1545551693aaefd34c9f157881cbb25331caba370fe9eccae671d21332123f01b12
|
7
|
+
data.tar.gz: 1389a2fd6cc8d60fbfa0269eeb087aed6edc86790cd6ecdab6475ff9cd530aae489833ee08156571e2ecc7605d0a4abc6f33943c514e4320510e597f6fa3fcfc
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
## [0.2.0](https://github.com/indigobio/hyperion/compare/v0.1.9...indigobio:v0.2.0) (2016-9-15)
|
2
|
+
|
3
|
+
- eliminate Mimic dependency to resolve conflicts in code that uses hyperion
|
4
|
+
|
1
5
|
## [0.1.7](https://github.com/indigobio/hyperion/compare/v0.1.6...indigobio:v0.1.7) (2015-12-14)
|
2
6
|
|
3
7
|
- fake_route now works with multipart body
|
@@ -390,4 +394,4 @@
|
|
390
394
|
it's shutting down, resulting in a timeout.
|
391
395
|
|
392
396
|
### 0.1.2
|
393
|
-
- Fixed broken gemspec version for abstractivator
|
397
|
+
- Fixed broken gemspec version for abstractivator
|
data/hyperion.gemspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
hyperion_http.gemspec
|
data/hyperion_http.gemspec
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'hyperion/aux/version'
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
6
|
spec.name = 'hyperion_http'
|
8
|
-
spec.version =
|
7
|
+
spec.version = '0.2.1'
|
9
8
|
spec.authors = ['Indigo BioAutomation, Inc.']
|
10
9
|
spec.email = ['pwinton@indigobio.com']
|
11
10
|
spec.summary = 'Ruby REST client'
|
@@ -30,6 +29,6 @@ Gem::Specification.new do |spec|
|
|
30
29
|
spec.add_runtime_dependency 'immutable_struct', '~> 1.1'
|
31
30
|
spec.add_runtime_dependency 'oj', '~> 2.12'
|
32
31
|
spec.add_runtime_dependency 'typhoeus', '~> 0.7'
|
33
|
-
spec.add_runtime_dependency '
|
32
|
+
spec.add_runtime_dependency 'rack'
|
34
33
|
spec.add_runtime_dependency 'logatron'
|
35
34
|
end
|
data/lib/hyperion/aux/logger.rb
CHANGED
data/lib/hyperion_test/fake.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'immutable_struct'
|
2
|
-
require 'mimic'
|
3
2
|
require 'hyperion/headers'
|
4
3
|
require 'hyperion/formats'
|
5
4
|
require 'uri'
|
@@ -7,27 +6,37 @@ require 'hyperion_test/fake_server'
|
|
7
6
|
|
8
7
|
class Hyperion
|
9
8
|
class << self
|
10
|
-
#
|
11
|
-
#
|
9
|
+
# Maintains a collection of fake servers, one for each base_uri.
|
10
|
+
# Manages rspec integration for automatic teardown after each test.
|
12
11
|
|
13
12
|
include Formats
|
14
13
|
include Headers
|
15
14
|
include TestFrameworkHooks
|
16
15
|
include Logger
|
17
16
|
|
17
|
+
# Configure routes on the server for the given base_uri
|
18
18
|
def fake(base_uri, &routes)
|
19
19
|
base_uri = normalized_base(base_uri)
|
20
|
-
|
21
|
-
|
22
|
-
@
|
20
|
+
unless @configured
|
21
|
+
hook_reset if can_hook_reset? && !reset_registered?
|
22
|
+
@configured = true
|
23
23
|
end
|
24
24
|
servers[base_uri].configure(&routes)
|
25
25
|
end
|
26
26
|
|
27
|
-
|
27
|
+
# Clear routes but don't stop servers. Meant to be called between tests.
|
28
|
+
# Starting/stopping servers is relatively slow. They can be reused.
|
29
|
+
def reset
|
30
|
+
servers.values.each(&:clear_routes)
|
31
|
+
@configured = false
|
32
|
+
end
|
33
|
+
|
34
|
+
# Stop all servers. This should only need to be called by tests that use
|
35
|
+
# Kim directly (like kim_spec.rb).
|
36
|
+
def teardown_cached_servers
|
28
37
|
servers.values.each(&:teardown)
|
29
38
|
servers.clear
|
30
|
-
@
|
39
|
+
@configured = false
|
31
40
|
end
|
32
41
|
|
33
42
|
private
|
@@ -1,54 +1,86 @@
|
|
1
1
|
require 'hyperion_test/test_framework_hooks'
|
2
2
|
require 'hyperion/aux/hash_ext'
|
3
|
-
require '
|
3
|
+
require 'hyperion/headers'
|
4
|
+
require 'hyperion/formats'
|
4
5
|
require 'hyperion_test/fake_server/types'
|
5
6
|
require 'hyperion_test/fake_server/config'
|
7
|
+
require 'hyperion_test/kim'
|
8
|
+
require 'hyperion_test/kim/matchers'
|
6
9
|
|
7
10
|
class Hyperion
|
8
11
|
class FakeServer
|
9
|
-
# Runs a
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
# Runs a Kim server configured per the specified routing rules.
|
13
|
+
include Kim::Matchers
|
14
|
+
include Headers
|
15
|
+
include Formats
|
13
16
|
|
14
|
-
attr_accessor :port
|
17
|
+
attr_accessor :port
|
15
18
|
|
16
19
|
def initialize(port)
|
17
20
|
@port = port
|
18
|
-
@
|
21
|
+
@kim = Kim.new(port: port)
|
22
|
+
@kim.start
|
19
23
|
end
|
20
24
|
|
21
25
|
def configure(&configure_routes)
|
22
26
|
config = Config.new
|
23
27
|
configure_routes.call(config)
|
24
|
-
|
25
|
-
|
28
|
+
config.rules.each do |rule|
|
29
|
+
matcher = Kim::Matcher.and(verb(rule.verb),
|
30
|
+
res(rule.path),
|
31
|
+
req_headers(rule.headers))
|
32
|
+
handler = wrap(rule.handler, rule.rest_route)
|
33
|
+
@kim.add_handler(matcher, &handler)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear_routes
|
38
|
+
@kim.clear_handlers
|
26
39
|
end
|
27
40
|
|
28
41
|
def teardown
|
29
|
-
|
30
|
-
|
31
|
-
|
42
|
+
@kim.stop
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Make it easier to write handlers by massaging input and output
|
48
|
+
def wrap(handler, rest_route)
|
49
|
+
proc do |req|
|
50
|
+
massage_request!(req)
|
51
|
+
resp = handler.call(req)
|
52
|
+
massage_response(resp, rest_route)
|
53
|
+
end
|
32
54
|
end
|
33
55
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
if @mimic_running
|
38
|
-
Mimic.cleanup!
|
39
|
-
Mimic::Server.instance.instance_variable_get(:@thread).join
|
56
|
+
def massage_request!(req)
|
57
|
+
if req.body && !req.body.empty?
|
58
|
+
req.body = read(req.body, :json)
|
40
59
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def massage_response(resp, rest_route)
|
63
|
+
if rack_response?(resp)
|
64
|
+
code, headers, body = resp
|
65
|
+
unless body.is_a?(String)
|
66
|
+
body = write(body, :json)
|
49
67
|
end
|
68
|
+
[code, headers, body]
|
69
|
+
else
|
70
|
+
if rest_route
|
71
|
+
rd = rest_route.response_descriptor
|
72
|
+
content_type = content_type_for(rd)
|
73
|
+
format = rd
|
74
|
+
else
|
75
|
+
content_type = 'application/json'
|
76
|
+
format = :json
|
77
|
+
end
|
78
|
+
['200', {'Content-Type' => content_type}, write(resp, format)]
|
50
79
|
end
|
51
|
-
|
80
|
+
end
|
81
|
+
|
82
|
+
def rack_response?(resp)
|
83
|
+
resp.is_a?(Array) && resp.size == 3
|
52
84
|
end
|
53
85
|
end
|
54
86
|
end
|
@@ -23,12 +23,11 @@ class Hyperion
|
|
23
23
|
def allowed_rule(args, handler)
|
24
24
|
if args.size == 1 && args.first.is_a?(RestRoute)
|
25
25
|
route = args.first
|
26
|
-
Rule.new(
|
26
|
+
Rule.new(route.method, route.uri.path, route_headers(route), handler, route)
|
27
27
|
else
|
28
|
-
# TODO: deprecate this
|
29
28
|
method, path, headers = args
|
30
29
|
headers ||= {}
|
31
|
-
Rule.new(
|
30
|
+
Rule.new(method, path, headers, handler, nil)
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
class Hyperion
|
2
2
|
class FakeServer
|
3
|
-
|
4
|
-
Rule
|
3
|
+
Rule = ImmutableStruct.new(:method, :path, :headers, :handler, :rest_route)
|
4
|
+
class Rule
|
5
|
+
alias_method :verb, :method
|
6
|
+
end
|
5
7
|
Request = ImmutableStruct.new(:body)
|
6
8
|
end
|
7
9
|
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'thread'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
require 'hyperion_test/kim/matcher'
|
7
|
+
|
8
|
+
class Hyperion
|
9
|
+
class Kim
|
10
|
+
# A dumb fake web server.
|
11
|
+
# This is minimal object wrapper around Rack/WEBrick. WEBrick was chosen
|
12
|
+
# because it comes with ruby and we're not doing rocket science here.
|
13
|
+
# Kim runs Rack/WEBrick in a separate thread and keeps an array of
|
14
|
+
# handlers. A handler is simply a predicate on a request object
|
15
|
+
# and a function to handle the request should the predicate return truthy.
|
16
|
+
# When rack notifies us of a request, we dispatch it to the first handler
|
17
|
+
# with a truthy predicate.
|
18
|
+
#
|
19
|
+
# Again, what we're trying to do is very simple. Most of the existing complexity
|
20
|
+
# is due to
|
21
|
+
# - thread synchronization
|
22
|
+
# - unmangling WEBrick's header renaming
|
23
|
+
# - loosening the requirements on what a handler function must return
|
24
|
+
#
|
25
|
+
# To support path parameters (e.g., /people/:name), a predicate may return
|
26
|
+
# a Request object as a truthy value, augmented with additional params.
|
27
|
+
# When the predicate returns a Request, the augmented request object is
|
28
|
+
# passed to the handler function in place of the original request.
|
29
|
+
|
30
|
+
Handler = Struct.new(:pred, :func)
|
31
|
+
Request = Struct.new(:verb, # 'GET' | 'POST' | ...
|
32
|
+
:path, # String
|
33
|
+
:params, # OpenStruct
|
34
|
+
:headers, # Hash[String => String]
|
35
|
+
:body) # String
|
36
|
+
class Request
|
37
|
+
alias_method :method, :verb
|
38
|
+
def merge(other)
|
39
|
+
merge_params(other.params)
|
40
|
+
end
|
41
|
+
def merge_params(other_params)
|
42
|
+
params = OpenStruct.new(self.params.to_h.merge(other_params.to_h))
|
43
|
+
Request.new(verb, path, params, headers, body)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(port:)
|
48
|
+
@port = port
|
49
|
+
@handlers = []
|
50
|
+
@lock = Mutex.new # controls access to this instance of Kim (via public methods and callbacks)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.webrick_mutex
|
54
|
+
@webrick_mutex ||= Mutex.new # controls access to the Rack::Handler::WEBrick singleton
|
55
|
+
end
|
56
|
+
|
57
|
+
def start
|
58
|
+
# Notes on synchronization:
|
59
|
+
#
|
60
|
+
# The only way to start a handler is with static method ::run
|
61
|
+
# which touches singleton instance variables. webrick_mutex
|
62
|
+
# ensures only one thread is in the singleton at a time.
|
63
|
+
#
|
64
|
+
# A threadsafe queue is used to notify the calling thread
|
65
|
+
# that the server thread has started. The caller needs to
|
66
|
+
# wait so it can obtain the webrick instance.
|
67
|
+
|
68
|
+
@lock.synchronize do
|
69
|
+
raise 'Cannot restart' if @stopped
|
70
|
+
Kim.webrick_mutex.synchronize do
|
71
|
+
q = Queue.new
|
72
|
+
@thread = Thread.start do
|
73
|
+
begin
|
74
|
+
opts = {Port: @port, Logger: ::Logger.new('/dev/null'), AccessLog: []} # hide output
|
75
|
+
Rack::Handler::WEBrick.run(method(:handle_request), opts) do |webrick|
|
76
|
+
q.push(webrick)
|
77
|
+
end
|
78
|
+
ensure
|
79
|
+
$stderr.puts "Hyperion fake server on port #{@port} exited unexpectedly!" unless @stopped
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@webrick = q.pop
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def stop
|
88
|
+
@lock.synchronize do
|
89
|
+
return if @stopped
|
90
|
+
@stopped = true
|
91
|
+
@webrick.shutdown
|
92
|
+
@thread.join
|
93
|
+
@webrick = nil
|
94
|
+
@thread = nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Add a handler. Returns a proc that removes the handler.
|
99
|
+
def add_handler(matcher_or_pred, &handler_proc)
|
100
|
+
@lock.synchronize do
|
101
|
+
handler = Handler.new(Matcher.wrap(matcher_or_pred), handler_proc)
|
102
|
+
@handlers.unshift(handler)
|
103
|
+
remover = proc { @lock.synchronize { @handlers.delete(handler) } }
|
104
|
+
remover
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def clear_handlers
|
109
|
+
@lock.synchronize do
|
110
|
+
@handlers = []
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
def handle_request(env)
|
116
|
+
@lock.synchronize do
|
117
|
+
req = request_for(env)
|
118
|
+
x = handle(req)
|
119
|
+
x = massage_response(x)
|
120
|
+
x = validate_response(x)
|
121
|
+
x
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def request_for(env)
|
126
|
+
verb = env['REQUEST_METHOD']
|
127
|
+
path = env['PATH_INFO']
|
128
|
+
params = OpenStruct.new(read_query_params(env['QUERY_STRING']))
|
129
|
+
headers = read_headers(env)
|
130
|
+
body = env['rack.input'].gets
|
131
|
+
Request.new(verb, path, params, headers, body)
|
132
|
+
end
|
133
|
+
|
134
|
+
def read_query_params(query_string)
|
135
|
+
query_string
|
136
|
+
.split('&')
|
137
|
+
.map { |kv| kv.split('=') }
|
138
|
+
.to_h
|
139
|
+
end
|
140
|
+
|
141
|
+
def read_headers(env)
|
142
|
+
# similar to https://github.com/ruby/ruby/blob/32674b167bddc0d737c38f84722986b0f228b44b/lib/webrick/cgi.rb#L217-L226
|
143
|
+
env.each_pair
|
144
|
+
.select { |k, _| mangled_header?(k) }
|
145
|
+
.map { |k, v| [unmangle_header_key(k), v] }
|
146
|
+
.to_h
|
147
|
+
end
|
148
|
+
|
149
|
+
def mangled_header?(h)
|
150
|
+
h.start_with?('HTTP_') || %w(CONTENT_TYPE CONTENT_LENGTH).include?(h)
|
151
|
+
end
|
152
|
+
|
153
|
+
def unmangle_header_key(k)
|
154
|
+
k.gsub(/^HTTP_/, '')
|
155
|
+
.split('_')
|
156
|
+
.map(&:titlecase)
|
157
|
+
.join('-')
|
158
|
+
end
|
159
|
+
|
160
|
+
def handle(req)
|
161
|
+
pred_value, func = @handlers.lazy
|
162
|
+
.map { |h| [h.pred.call(req), h.func] }
|
163
|
+
.select { |(pv, _)| pv }
|
164
|
+
.first || [nil, no_route_matched_func]
|
165
|
+
func.call(pred_value.is_a?(Request) ? pred_value : req)
|
166
|
+
end
|
167
|
+
|
168
|
+
def massage_response(r)
|
169
|
+
if triplet?(r)
|
170
|
+
r[0] = r[0].to_s # code
|
171
|
+
r[1] = r[1] || {} # headers
|
172
|
+
r[2] = *r[2] # body/bodies (coerce to array)
|
173
|
+
r[2].map!(&:to_s)
|
174
|
+
r
|
175
|
+
elsif r.is_a?(String)
|
176
|
+
['200', {}, [r]]
|
177
|
+
else
|
178
|
+
r
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def validate_response(r)
|
183
|
+
triplet?(r) or return server_error("Invalid response, not a size-3 array: #{r.inspect}.")
|
184
|
+
http_code?(r[0]) or return server_error("Invalid response, invalid http code: #{r[0].inspect}")
|
185
|
+
headers?(r[1]) or return server_error("Invalid response, invalid header hash: #{r[1].inspect}")
|
186
|
+
bodies?(r[2]) or return server_error("Invalid response, invalid bodies array: #{r[2].inspect}")
|
187
|
+
r
|
188
|
+
end
|
189
|
+
|
190
|
+
def no_route_matched_func
|
191
|
+
proc do
|
192
|
+
['404', error_headers, ['Request matched no routes.']]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def triplet?(x)
|
197
|
+
x.is_a?(Array) && x.size == 3
|
198
|
+
end
|
199
|
+
|
200
|
+
def http_code?(x)
|
201
|
+
return false unless x.respond_to?(:to_i)
|
202
|
+
v = x.to_i
|
203
|
+
100 <= v && v < 600
|
204
|
+
end
|
205
|
+
|
206
|
+
def headers?(x)
|
207
|
+
# TODO: check for valid keys and values
|
208
|
+
x.is_a?(Hash)
|
209
|
+
end
|
210
|
+
|
211
|
+
def bodies?(x)
|
212
|
+
x.is_a?(Array) && x.all? { |v| v.is_a?(String) }
|
213
|
+
end
|
214
|
+
|
215
|
+
def server_error(msg)
|
216
|
+
['500', error_headers, [msg]]
|
217
|
+
end
|
218
|
+
|
219
|
+
def error_headers
|
220
|
+
{'Content-Type' => 'text/plain'}
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
class Hyperion
|
2
|
+
class Kim
|
3
|
+
class Matcher
|
4
|
+
# Fancy predicates for HTTP requests.
|
5
|
+
# Features:
|
6
|
+
# - and/or/not combinators
|
7
|
+
# - If a predicate raises an error, it is caught and treated as falsey. simplifies predicates.
|
8
|
+
# For example: headers['Allow'].starts_with?('application/')
|
9
|
+
# will raise if no Allow header was sent, however we really just want to treat that as
|
10
|
+
# a non-match.
|
11
|
+
# - Parameter extraction. A matcher can return an augmented Request as the truthy value.
|
12
|
+
|
13
|
+
attr_reader :func
|
14
|
+
|
15
|
+
def initialize(func=nil, &block)
|
16
|
+
@func = Matcher.wrap(block || func)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(req)
|
20
|
+
@func.call(req)
|
21
|
+
end
|
22
|
+
|
23
|
+
def and(other)
|
24
|
+
Matcher.new do |req|
|
25
|
+
(req2 = @func.call(req)) && other.call(req2)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def or(other)
|
30
|
+
Matcher.new do |req|
|
31
|
+
@func.call(req) || other.call(req)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def not
|
36
|
+
Matcher.new do |req|
|
37
|
+
@func.call(req) ? nil : req
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.and(*ms)
|
42
|
+
m, *rest = ms
|
43
|
+
if rest.empty?
|
44
|
+
m
|
45
|
+
else
|
46
|
+
m.and(Matcher.and(*rest))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Coerce the return value of the function to nil/hash in case
|
53
|
+
# it returns a simple true/false.
|
54
|
+
# Update (mutate) the request params with any addition values
|
55
|
+
# gleaned by a successful match.
|
56
|
+
def self.wrap(f)
|
57
|
+
if f.is_a?(Matcher)
|
58
|
+
f
|
59
|
+
else
|
60
|
+
proc do |req|
|
61
|
+
v = coerce(f, req)
|
62
|
+
# Update the request parameters. respond_to?(:merge) is a
|
63
|
+
# compromise between outright depending on Kim::Request
|
64
|
+
# and threading a totally generic 'update' function
|
65
|
+
# through all the matcher code.
|
66
|
+
if v && req.respond_to?(:merge)
|
67
|
+
req.merge(v)
|
68
|
+
else
|
69
|
+
v
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.coerce(f, req)
|
76
|
+
case v = f.call(req)
|
77
|
+
when TrueClass then req
|
78
|
+
when FalseClass then nil
|
79
|
+
else v
|
80
|
+
end
|
81
|
+
rescue
|
82
|
+
nil # treat predicate errors as falsey
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
require 'hyperion_test/kim/matcher'
|
3
|
+
|
4
|
+
class Hyperion
|
5
|
+
class Kim
|
6
|
+
module Matchers
|
7
|
+
# Some useful matchers to include in your code
|
8
|
+
|
9
|
+
def res(resource_pattern)
|
10
|
+
regex = resource_pattern.gsub(/:([^\/]+)/, "(?<\\1>[^\\/]+)")
|
11
|
+
Matcher.new do |req|
|
12
|
+
m = req.path.match(regex)
|
13
|
+
m && req.merge_params(m.names.zip(m.captures).to_h)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def verb(verb_to_match)
|
18
|
+
Matcher.new do |req|
|
19
|
+
req.verb.to_s.upcase == verb_to_match.to_s.upcase
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def req_headers(required_headers)
|
24
|
+
Matcher.new do |req|
|
25
|
+
required_headers.each_pair.all? do |(k, v)|
|
26
|
+
hash_includes?(req.headers.to_h, k, v)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def req_params(required_params)
|
32
|
+
Matcher.new do |req|
|
33
|
+
required_params.each_pair.all? do |(k, v)|
|
34
|
+
hash_includes?(req.params.to_h, k, v)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def hash_includes?(h, k, v)
|
42
|
+
(h.keys.include?(k.to_s) || h.keys.include?(k.to_sym)) && (v.nil? || (h[k.to_s] || h[k.to_sym]) == v)
|
43
|
+
end
|
44
|
+
|
45
|
+
extend self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -2,19 +2,19 @@ require 'hyperion'
|
|
2
2
|
require 'rspec/core'
|
3
3
|
|
4
4
|
module TestFrameworkHooks
|
5
|
-
def
|
5
|
+
def reset_registered?
|
6
6
|
rspec_after_example_hooks.any? do |hook_proc|
|
7
|
-
hook_proc.source_location == method(:
|
7
|
+
hook_proc.source_location == method(:reset).to_proc.source_location
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
RSpec.current_example
|
11
|
+
def can_hook_reset?
|
12
|
+
!!RSpec.current_example
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
15
|
+
def hook_reset
|
16
16
|
hyperion = self
|
17
|
-
rspec_hooks.register(:prepend, :after, :each) { hyperion.
|
17
|
+
rspec_hooks.register(:prepend, :after, :each) { hyperion.reset }
|
18
18
|
end
|
19
19
|
|
20
20
|
def rspec_after_example_hooks
|
@@ -110,12 +110,11 @@ describe Hyperion::Requestor do
|
|
110
110
|
context 'on a 400-level response' do
|
111
111
|
|
112
112
|
def example(opts)
|
113
|
-
|
113
|
+
code = ((400..499).to_a - [404]).sample
|
114
|
+
arrange(:get, [code, {}, opts[:response]])
|
114
115
|
expect{request(@route)}.to raise_error HyperionError, opts[:error]
|
115
116
|
end
|
116
117
|
|
117
|
-
|
118
|
-
|
119
118
|
context 'when the response is a properly-formed error message' do
|
120
119
|
it 'raises an error with the response message' do
|
121
120
|
example response: '{"message":"oops"}',
|
@@ -73,8 +73,8 @@ describe Hyperion do
|
|
73
73
|
end
|
74
74
|
|
75
75
|
it 'logs requests for unstubbed routes' do
|
76
|
-
route1 = RestRoute.new(:get, 'http://
|
77
|
-
route2 = RestRoute.new(:get, 'http://
|
76
|
+
route1 = RestRoute.new(:get, 'http://somesite.org/stuff', user_response_params)
|
77
|
+
route2 = RestRoute.new(:get, 'http://somesite.org/things', user_response_params)
|
78
78
|
Hyperion.fake(route1.uri.base) do |svr|
|
79
79
|
svr.allow(route1) {{}}
|
80
80
|
end
|
@@ -136,23 +136,23 @@ describe Hyperion do
|
|
136
136
|
end
|
137
137
|
|
138
138
|
it 'allows multiple fake servers to be created' do
|
139
|
-
Hyperion.fake('http://
|
140
|
-
svr.allow(:get, '/welcome') { success_response({'text' => 'hello from
|
139
|
+
Hyperion.fake('http://somesite.org') do |svr|
|
140
|
+
svr.allow(:get, '/welcome') { success_response({'text' => 'hello from somesite'}) }
|
141
141
|
end
|
142
142
|
|
143
|
-
Hyperion.fake('http://indigo.com:
|
144
|
-
svr.allow(:get, '/welcome') { success_response({'text' => 'hello from indigo@
|
143
|
+
Hyperion.fake('http://indigo.com:80') do |svr|
|
144
|
+
svr.allow(:get, '/welcome') { success_response({'text' => 'hello from indigo@80'}) }
|
145
145
|
end
|
146
146
|
|
147
147
|
Hyperion.fake('http://indigo.com:4000') do |svr|
|
148
148
|
svr.allow(:get, '/welcome') { success_response({'text' => 'hello from indigo@4000'}) }
|
149
149
|
end
|
150
150
|
|
151
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://
|
152
|
-
expect(result.body).to eql({'text' => 'hello from
|
151
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://somesite.org/welcome', user_response_params))
|
152
|
+
expect(result.body).to eql({'text' => 'hello from somesite'})
|
153
153
|
|
154
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://indigo.com:
|
155
|
-
expect(result.body).to eql({'text' => 'hello from indigo@
|
154
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://indigo.com:80/welcome', user_response_params))
|
155
|
+
expect(result.body).to eql({'text' => 'hello from indigo@80'})
|
156
156
|
|
157
157
|
result = Hyperion.request(RestRoute.new(:get, 'http://indigo.com:4000/welcome', user_response_params))
|
158
158
|
expect(result.body).to eql({'text' => 'hello from indigo@4000'})
|
@@ -176,37 +176,37 @@ describe Hyperion do
|
|
176
176
|
end
|
177
177
|
|
178
178
|
it 'allows routes to be augmented' do
|
179
|
-
Hyperion.fake('http://
|
179
|
+
Hyperion.fake('http://somesite.org') do |svr|
|
180
180
|
svr.allow(:get, '/old') { success_response({'text' => 'old'}) }
|
181
181
|
svr.allow(:get, '/hello') { success_response({'text' => 'hello'}) }
|
182
182
|
svr.allow(RestRoute.new(:get, '/users/0', user_response_params)) { success_response({'user' => 'old user'}) }
|
183
183
|
end
|
184
184
|
|
185
185
|
# smoke test that the server is up and running
|
186
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://
|
186
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://somesite.org/hello', user_response_params))
|
187
187
|
expect(result.body).to eql({'text' => 'hello'})
|
188
188
|
|
189
189
|
# augment the routes
|
190
|
-
Hyperion.fake('http://
|
190
|
+
Hyperion.fake('http://somesite.org') do |svr|
|
191
191
|
svr.allow(:get, '/hello') { success_response({'text' => 'aloha'}) }
|
192
192
|
svr.allow(:get, '/goodbye') { success_response({'text' => 'goodbye'}) }
|
193
193
|
svr.allow(RestRoute.new(:get, '/users/0', user_response_params)) { success_response({'user' => 'new user'}) }
|
194
194
|
end
|
195
195
|
|
196
196
|
# untouched routes are left alone
|
197
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://
|
197
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://somesite.org/old', user_response_params))
|
198
198
|
expect(result.body).to eql({'text' => 'old'})
|
199
199
|
|
200
200
|
# restating the route replaces it (last one wins)
|
201
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://
|
201
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://somesite.org/hello', user_response_params))
|
202
202
|
expect(result.body).to eql({'text' => 'aloha'})
|
203
203
|
|
204
204
|
# new routes can be added
|
205
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://
|
205
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://somesite.org/goodbye', user_response_params))
|
206
206
|
expect(result.body).to eql({'text' => 'goodbye'})
|
207
207
|
|
208
208
|
# restating a route routes that uses headers to differentiate replaces it (last one wins)
|
209
|
-
result = Hyperion.request(RestRoute.new(:get, 'http://
|
209
|
+
result = Hyperion.request(RestRoute.new(:get, 'http://somesite.org/users/0', user_response_params))
|
210
210
|
expect(result.body).to eql({'user' => 'new user'})
|
211
211
|
end
|
212
212
|
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'hyperion_test/kim'
|
3
|
+
require 'hyperion_test/kim/matcher'
|
4
|
+
require 'hyperion_test/kim/matchers'
|
5
|
+
|
6
|
+
describe Hyperion::Kim::Matcher do
|
7
|
+
Matcher = Hyperion::Kim::Matcher
|
8
|
+
Request = Hyperion::Kim::Request
|
9
|
+
include Hyperion::Kim::Matchers
|
10
|
+
|
11
|
+
describe '::new' do
|
12
|
+
it 'creates a matcher' do
|
13
|
+
m = Matcher.new(proc { |x| x >= 0 })
|
14
|
+
expect(m.call(1)).to be_truthy
|
15
|
+
expect(m.call(-1)).to be_falsey
|
16
|
+
end
|
17
|
+
it 'accepts a block' do
|
18
|
+
m = Matcher.new { |x| x >= 0 }
|
19
|
+
expect(m.call(1)).to be_truthy
|
20
|
+
expect(m.call(-1)).to be_falsey
|
21
|
+
end
|
22
|
+
it 'can return a request' do
|
23
|
+
m = res('/greet/:name')
|
24
|
+
r = m.call(req('/greet/kim'))
|
25
|
+
expect(r.params.name).to eql 'kim'
|
26
|
+
expect(m.call(req('/text/kim'))).to be_falsey
|
27
|
+
end
|
28
|
+
it 'treats errors as falsey' do
|
29
|
+
m = Matcher.new { raise 'oops' }
|
30
|
+
expect(m.call(:anything)).to be_falsey
|
31
|
+
end
|
32
|
+
end
|
33
|
+
describe '#call' do
|
34
|
+
it 'invokes the predicate' do
|
35
|
+
m = Matcher.new { |x| x >= 0 }
|
36
|
+
expect(m.call(1)).to be_truthy
|
37
|
+
expect(m.call(-1)).to be_falsey
|
38
|
+
end
|
39
|
+
end
|
40
|
+
describe '#and' do
|
41
|
+
it 'combines predicates with boolean AND' do
|
42
|
+
positive = Matcher.new { |x| x > 0 }
|
43
|
+
even = Matcher.new(&:even?)
|
44
|
+
m = positive.and(even)
|
45
|
+
expect(m.call(2)).to be_truthy
|
46
|
+
expect(m.call(-2)).to be_falsey
|
47
|
+
expect(m.call(1)).to be_falsey
|
48
|
+
expect(m.call(-1)).to be_falsey
|
49
|
+
end
|
50
|
+
it 'merges result hash' do
|
51
|
+
matcher = res('/greet/:name').and(res('/:action/kim'))
|
52
|
+
verify_path_match matcher, '/greet/kim', yields_params: {name: 'kim', action: 'greet'}
|
53
|
+
verify_path_does_not_match matcher, '/greet/bob'
|
54
|
+
verify_path_does_not_match matcher, '/text/kim'
|
55
|
+
verify_path_does_not_match matcher, '/text/bob'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
describe '#or' do
|
59
|
+
it 'combines predicates with boolean OR' do
|
60
|
+
positive = Matcher.new { |x| x > 0 }
|
61
|
+
even = Matcher.new(&:even?)
|
62
|
+
m = positive.or(even)
|
63
|
+
expect(m.call(2)).to be_truthy
|
64
|
+
expect(m.call(-2)).to be_truthy
|
65
|
+
expect(m.call(1)).to be_truthy
|
66
|
+
expect(m.call(-1)).to be_falsey
|
67
|
+
end
|
68
|
+
it 'returns result hash of first matching predicate' do
|
69
|
+
matcher = res('/greet/:name').or(res('/:action/kim'))
|
70
|
+
verify_path_match matcher, '/greet/kim', yields_params: {name: 'kim'}
|
71
|
+
verify_path_match matcher, '/text/kim', yields_params: {action: 'text'}
|
72
|
+
verify_path_does_not_match matcher, '/text/bob'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
describe '#not' do
|
76
|
+
it 'returns a negated predicate' do
|
77
|
+
even = Matcher.new(&:even?)
|
78
|
+
odd = even.not
|
79
|
+
expect(odd.call(1)).to be_truthy
|
80
|
+
expect(odd.call(2)).to be_falsey
|
81
|
+
end
|
82
|
+
end
|
83
|
+
describe '::and' do
|
84
|
+
it 'combines predicates with boolean AND' do
|
85
|
+
positive = Matcher.new { |x| x > 0 }
|
86
|
+
even = Matcher.new(&:even?)
|
87
|
+
mult10 = Matcher.new { |x| x % 10 == 0 }
|
88
|
+
m = Matcher.and(positive, even, mult10)
|
89
|
+
expect(m.call(10)).to be_truthy
|
90
|
+
expect(m.call(-10)).to be_falsey
|
91
|
+
end
|
92
|
+
end
|
93
|
+
context 'when the request is augmented with params' do
|
94
|
+
let(:matcher) do
|
95
|
+
match_people = res('/people/:name')
|
96
|
+
name_starts_with_k = Matcher.new { |r| r.params.name.start_with?('k') }
|
97
|
+
match_people.and(name_starts_with_k)
|
98
|
+
end
|
99
|
+
it 'the params are updated as the predicate executes' do
|
100
|
+
verify_path_match matcher, '/people/kim'
|
101
|
+
verify_path_does_not_match matcher, '/people/kim'
|
102
|
+
verify_path_does_not_match matcher, '/people/bob'
|
103
|
+
verify_path_does_not_match matcher, '/idiots/kanye'
|
104
|
+
end
|
105
|
+
it 'the original request is unchanged' do
|
106
|
+
original_request = req('/people/kim')
|
107
|
+
augmented_request = matcher.call(original_request)
|
108
|
+
expect(augmented_request.params.name).to eql 'kim'
|
109
|
+
expect(original_request.params.name).to be nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def req(path)
|
114
|
+
Request.new('GET', path, OpenStruct.new, {}, nil)
|
115
|
+
end
|
116
|
+
|
117
|
+
def verify_path_match(matcher, path, yields_params: nil)
|
118
|
+
result = matcher.call(req(path))
|
119
|
+
expect(result).to be_truthy
|
120
|
+
expect(result.params.to_h).to eql(yields_params) if yields_params
|
121
|
+
end
|
122
|
+
|
123
|
+
def verify_path_does_not_match(matcher, path)
|
124
|
+
expect(matcher.call(res(path))).to be_falsey
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'hyperion_test/kim'
|
3
|
+
require 'hyperion_test/kim/matchers'
|
4
|
+
|
5
|
+
describe Hyperion::Kim::Matchers do
|
6
|
+
include Hyperion::Kim::Matchers
|
7
|
+
describe '#res' do
|
8
|
+
it 'matches resource paths' do
|
9
|
+
m = res('/people/:name/birthplace/:city')
|
10
|
+
r = m.call(req(path: '/people/kim/birthplace/la'))
|
11
|
+
expect(r.params.name).to eql 'kim'
|
12
|
+
expect(r.params.city).to eql 'la'
|
13
|
+
expect(m.call(req(path: '/people/kim/home/la'))).to be_falsey
|
14
|
+
end
|
15
|
+
end
|
16
|
+
describe '#verb' do
|
17
|
+
it 'matches the HTTP verb' do
|
18
|
+
expect(verb('GET').call(req(verb: 'GET'))).to be_truthy
|
19
|
+
expect(verb('PUT').call(req(verb: 'put'))).to be_truthy
|
20
|
+
expect(verb(:post).call(req(verb: 'POST'))).to be_truthy
|
21
|
+
expect(verb('GET').call(req(verb: 'PUT'))).to be_falsey
|
22
|
+
end
|
23
|
+
end
|
24
|
+
describe '#req_headers' do
|
25
|
+
it 'matches headers' do
|
26
|
+
m = req_headers('Allow' => 'application/json')
|
27
|
+
expect(m.call(req(headers: {'Allow' => 'application/json'}))).to be_truthy
|
28
|
+
expect(m.call(req(headers: {'Allow' => 'text/html'}))).to be_falsey
|
29
|
+
expect(m.call(req(headers: {}))).to be_falsey
|
30
|
+
end
|
31
|
+
it 'only checks for presence if value is nil' do
|
32
|
+
m = req_headers('Allow' => nil)
|
33
|
+
expect(m.call(req(headers: {'Allow' => 'application/json'}))).to be_truthy
|
34
|
+
expect(m.call(req(headers: {'Allow' => 'text/html'}))).to be_truthy
|
35
|
+
expect(m.call(req(headers: {}))).to be_falsey
|
36
|
+
end
|
37
|
+
end
|
38
|
+
describe '#req_params' do
|
39
|
+
it 'matches params' do
|
40
|
+
m = req_params(a: 1, 'b' => 2)
|
41
|
+
expect(m.call(req(params: params(a: 1, b: 2)))).to be_truthy
|
42
|
+
end
|
43
|
+
it 'only checks for presence if value is nil' do
|
44
|
+
m = req_params(c: nil)
|
45
|
+
expect(m.call(req(params: params(c: 1)))).to be_truthy
|
46
|
+
expect(m.call(req(params: params(c: 2)))).to be_truthy
|
47
|
+
expect(m.call(req(params: params(z: 2)))).to be_falsey
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def req(attrs)
|
52
|
+
Hyperion::Kim::Request.new(*attrs.values_at(*Hyperion::Kim::Request.members))
|
53
|
+
end
|
54
|
+
|
55
|
+
def params(*args)
|
56
|
+
OpenStruct.new(*args)
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'hyperion_test/kim'
|
3
|
+
require 'typhoeus'
|
4
|
+
require 'hyperion_test'
|
5
|
+
|
6
|
+
describe Hyperion::Kim do
|
7
|
+
before(:all) do
|
8
|
+
Hyperion.teardown_cached_servers
|
9
|
+
@port = 9001
|
10
|
+
@kim = Hyperion::Kim.new(port: @port)
|
11
|
+
@kim.start
|
12
|
+
end
|
13
|
+
before(:each) { @kim.clear_handlers }
|
14
|
+
after(:all) { @kim.stop }
|
15
|
+
attr_accessor :kim
|
16
|
+
|
17
|
+
let!(:always) { proc { true } }
|
18
|
+
|
19
|
+
context 'normal operation' do
|
20
|
+
it 'routes to a block' do
|
21
|
+
kim.add_handler(always) { 'Hello, World!' }
|
22
|
+
expect(get_body('/')).to eql 'Hello, World!'
|
23
|
+
end
|
24
|
+
it 'routes to the most recently added handler with a true predicate' do
|
25
|
+
kim.add_handler(proc{false}) { 'a' }
|
26
|
+
kim.add_handler(proc{true}) { 'b' }
|
27
|
+
kim.add_handler(proc{true}) { 'c' }
|
28
|
+
kim.add_handler(proc{false}) { 'd' }
|
29
|
+
expect(get_body('/')).to eql 'c'
|
30
|
+
end
|
31
|
+
it 'allows multiple servers running in parallel' do
|
32
|
+
kim2 = Hyperion::Kim.new(port: 9002)
|
33
|
+
begin
|
34
|
+
kim2.start
|
35
|
+
kim.add_handler(always) { '1' }
|
36
|
+
kim2.add_handler(always) { '2' }
|
37
|
+
expect(Typhoeus.get('http://localhost:9001').body).to eql '1'
|
38
|
+
expect(Typhoeus.get('http://localhost:9002').body).to eql '2'
|
39
|
+
kim2.stop
|
40
|
+
expect(Typhoeus.get('http://localhost:9001').body).to eql '1'
|
41
|
+
expect(Typhoeus.get('http://localhost:9002').success?).to be false
|
42
|
+
ensure
|
43
|
+
kim2.stop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
it 'continues working after a handler error' do
|
47
|
+
crash_command = proc { |r| r.path.include?('crash') }
|
48
|
+
greet_command = proc { |r| r.path.include?('greet') }
|
49
|
+
kim.add_handler(crash_command) { raise 'oops' }
|
50
|
+
kim.add_handler(greet_command) { 'hello' }
|
51
|
+
expect(get_code('/crash')).to eql 500
|
52
|
+
expect(get_code('/greet')).to eql 200
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#add_handler' do
|
57
|
+
it 'returns a remover proc' do
|
58
|
+
remover = kim.add_handler(always) { 'foo' }
|
59
|
+
expect(get_body('/')).to eql 'foo'
|
60
|
+
remover.call
|
61
|
+
expect(get_code('/')).to eql 404
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#clear_handlers' do
|
66
|
+
it 'clears all handlers' do
|
67
|
+
kim.add_handler(always) { 'foo' }
|
68
|
+
expect(get_body('/')).to eql 'foo'
|
69
|
+
kim.clear_handlers
|
70
|
+
expect(get_code('/')).to eql 404
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'a handler' do
|
75
|
+
it 'receives the HTTP verb' do
|
76
|
+
verb = nil
|
77
|
+
method = nil
|
78
|
+
kim.add_handler(always) do |r|
|
79
|
+
verb = r.verb
|
80
|
+
method = r.method
|
81
|
+
end
|
82
|
+
post('/')
|
83
|
+
expect(verb).to eql 'POST'
|
84
|
+
expect(method).to eql 'POST'
|
85
|
+
end
|
86
|
+
it 'receives the resource path' do
|
87
|
+
path = nil
|
88
|
+
kim.add_handler(always) { |r| path = r.path; '' }
|
89
|
+
get('/foo/bar')
|
90
|
+
expect(path).to eql '/foo/bar'
|
91
|
+
end
|
92
|
+
it 'receives the params' do
|
93
|
+
params = nil
|
94
|
+
kim.add_handler(proc{ |req| req.merge_params(d: '4', e: '5') }) { |r| params = r.params; '' }
|
95
|
+
get('/foo/bar?a=1&b=2&c=3')
|
96
|
+
expect(params.a).to eql '1'
|
97
|
+
expect(params[:b]).to eql '2'
|
98
|
+
expect(params['c']).to eql '3'
|
99
|
+
expect(params.d).to eql '4'
|
100
|
+
expect(params.e).to eql '5'
|
101
|
+
end
|
102
|
+
it 'receives the request headers' do
|
103
|
+
headers = nil
|
104
|
+
kim.add_handler(always) { |r| headers = r.headers; '' }
|
105
|
+
get('/', headers: {'Accept' => 'application/json', 'Content-Type' => 'text/html'})
|
106
|
+
expect(headers['Accept']).to eql 'application/json'
|
107
|
+
expect(headers['Content-Type']).to eql 'text/html'
|
108
|
+
end
|
109
|
+
it 'receives the request body' do
|
110
|
+
body = nil
|
111
|
+
kim.add_handler(always) { |r| body = r.body; '' }
|
112
|
+
post('/', body: 'please do something')
|
113
|
+
expect(body).to eql 'please do something'
|
114
|
+
end
|
115
|
+
it 'can return a string' do
|
116
|
+
kim.add_handler(always) { 'hello' }
|
117
|
+
r = get('/')
|
118
|
+
expect(r.code).to eql 200
|
119
|
+
expect(r.body).to eql 'hello'
|
120
|
+
end
|
121
|
+
it 'can return a rack response' do
|
122
|
+
kim.add_handler(always) { ['400', {'Content-Type' => 'application/greeting'}, ['oops']] }
|
123
|
+
r = get('/')
|
124
|
+
expect(r.body).to eql 'oops'
|
125
|
+
expect(r.code).to eql 400
|
126
|
+
expect(r.headers['Content-Type']).to eql 'application/greeting'
|
127
|
+
end
|
128
|
+
it 'rack response requirements are somewhat loosened' do
|
129
|
+
kim.add_handler(always) { [400, nil, 'oops'] }
|
130
|
+
r = get('/')
|
131
|
+
expect(r.body).to eql 'oops'
|
132
|
+
expect(r.code).to eql 400
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_body(path)
|
137
|
+
response = get(path)
|
138
|
+
expect(response.success?).to be true
|
139
|
+
response.body
|
140
|
+
end
|
141
|
+
|
142
|
+
def base_uri
|
143
|
+
"http://localhost:#{@port}"
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_code(path)
|
147
|
+
get(path).code
|
148
|
+
end
|
149
|
+
|
150
|
+
def get(path, headers: {})
|
151
|
+
Typhoeus.get(File.join(base_uri, path), headers: headers)
|
152
|
+
end
|
153
|
+
|
154
|
+
def post(path, headers: {}, body: nil)
|
155
|
+
Typhoeus.post(File.join(base_uri, path), headers: headers, body: body)
|
156
|
+
end
|
157
|
+
end
|
data/update_version.sh
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/bin/bash -e
|
2
|
+
|
3
|
+
#This script is used during the release process. It is not intended to be ran manually.
|
4
|
+
|
5
|
+
VERSION="$1"
|
6
|
+
VERSION="${VERSION:?"must provide version as first parameter"}"
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")"; pwd)"
|
8
|
+
|
9
|
+
updateVersion(){
|
10
|
+
updateGemspec
|
11
|
+
commitStagedFiles "Update version to ${VERSION}"
|
12
|
+
}
|
13
|
+
|
14
|
+
updateGemspec(){
|
15
|
+
echo -e "\nUpdating gemspec version"
|
16
|
+
local gemspecPath="${SCRIPT_DIR}/hyperion_http.gemspec"
|
17
|
+
sed -i 's/\(\.version\s*=\s*\).*/\1'"'${VERSION}'/" "${gemspecPath}"
|
18
|
+
stageFiles "${gemspecPath}"
|
19
|
+
}
|
20
|
+
|
21
|
+
stageAndCommit(){
|
22
|
+
local msg="$1"
|
23
|
+
shift
|
24
|
+
local files=( "$@" )
|
25
|
+
stageFiles "${files[@]}"
|
26
|
+
commitStagedFiles "${msg}"
|
27
|
+
}
|
28
|
+
|
29
|
+
stageFiles(){
|
30
|
+
local files=( "$@" )
|
31
|
+
git add "${files[@]}"
|
32
|
+
}
|
33
|
+
|
34
|
+
commitStagedFiles(){
|
35
|
+
local msg="$1"
|
36
|
+
if thereAreStagedFiles; then
|
37
|
+
git commit -m "${msg}"
|
38
|
+
else
|
39
|
+
echo "No changes to commit"
|
40
|
+
fi
|
41
|
+
}
|
42
|
+
|
43
|
+
thereAreStagedFiles(){
|
44
|
+
git update-index -q --ignore-submodules --refresh
|
45
|
+
if git diff-index --cached --quiet HEAD --ignore-submodules -- ; then
|
46
|
+
return 1;
|
47
|
+
else
|
48
|
+
return 0;
|
49
|
+
fi
|
50
|
+
}
|
51
|
+
|
52
|
+
updateVersion
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hyperion_http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Indigo BioAutomation, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -165,19 +165,19 @@ dependencies:
|
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0.7'
|
167
167
|
- !ruby/object:Gem::Dependency
|
168
|
-
name:
|
168
|
+
name: rack
|
169
169
|
requirement: !ruby/object:Gem::Requirement
|
170
170
|
requirements:
|
171
|
-
- -
|
171
|
+
- - ">="
|
172
172
|
- !ruby/object:Gem::Version
|
173
|
-
version: 0
|
173
|
+
version: '0'
|
174
174
|
type: :runtime
|
175
175
|
prerelease: false
|
176
176
|
version_requirements: !ruby/object:Gem::Requirement
|
177
177
|
requirements:
|
178
|
-
- -
|
178
|
+
- - ">="
|
179
179
|
- !ruby/object:Gem::Version
|
180
|
-
version: 0
|
180
|
+
version: '0'
|
181
181
|
- !ruby/object:Gem::Dependency
|
182
182
|
name: logatron
|
183
183
|
requirement: !ruby/object:Gem::Requirement
|
@@ -208,6 +208,7 @@ files:
|
|
208
208
|
- README.md
|
209
209
|
- Rakefile
|
210
210
|
- build.yml
|
211
|
+
- hyperion.gemspec
|
211
212
|
- hyperion_http.gemspec
|
212
213
|
- lib/hyperion.rb
|
213
214
|
- lib/hyperion/aux/bug_error.rb
|
@@ -215,7 +216,6 @@ files:
|
|
215
216
|
- lib/hyperion/aux/logger.rb
|
216
217
|
- lib/hyperion/aux/typho.rb
|
217
218
|
- lib/hyperion/aux/util.rb
|
218
|
-
- lib/hyperion/aux/version.rb
|
219
219
|
- lib/hyperion/formats.rb
|
220
220
|
- lib/hyperion/headers.rb
|
221
221
|
- lib/hyperion/hyperion.rb
|
@@ -238,8 +238,10 @@ files:
|
|
238
238
|
- lib/hyperion_test/fake.rb
|
239
239
|
- lib/hyperion_test/fake_server.rb
|
240
240
|
- lib/hyperion_test/fake_server/config.rb
|
241
|
-
- lib/hyperion_test/fake_server/dispatcher.rb
|
242
241
|
- lib/hyperion_test/fake_server/types.rb
|
242
|
+
- lib/hyperion_test/kim.rb
|
243
|
+
- lib/hyperion_test/kim/matcher.rb
|
244
|
+
- lib/hyperion_test/kim/matchers.rb
|
243
245
|
- lib/hyperion_test/spec_helper.rb
|
244
246
|
- lib/hyperion_test/test_framework_hooks.rb
|
245
247
|
- spec/fixtures/test
|
@@ -254,9 +256,13 @@ files:
|
|
254
256
|
- spec/lib/hyperion/types/hyperion_uri_spec.rb
|
255
257
|
- spec/lib/hyperion_spec.rb
|
256
258
|
- spec/lib/hyperion_test/fake_route_spec.rb
|
259
|
+
- spec/lib/hyperion_test/kim/matcher_spec.rb
|
260
|
+
- spec/lib/hyperion_test/kim/matchers_spec.rb
|
261
|
+
- spec/lib/hyperion_test/kim_spec.rb
|
257
262
|
- spec/lib/types_spec.rb
|
258
263
|
- spec/spec_helper.rb
|
259
264
|
- spec/support/core_helpers.rb
|
265
|
+
- update_version.sh
|
260
266
|
homepage: ''
|
261
267
|
licenses:
|
262
268
|
- MIT
|
@@ -277,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
277
283
|
version: '0'
|
278
284
|
requirements: []
|
279
285
|
rubyforge_project:
|
280
|
-
rubygems_version: 2.
|
286
|
+
rubygems_version: 2.6.2
|
281
287
|
signing_key:
|
282
288
|
specification_version: 4
|
283
289
|
summary: Ruby REST client
|
@@ -294,7 +300,9 @@ test_files:
|
|
294
300
|
- spec/lib/hyperion/types/hyperion_uri_spec.rb
|
295
301
|
- spec/lib/hyperion_spec.rb
|
296
302
|
- spec/lib/hyperion_test/fake_route_spec.rb
|
303
|
+
- spec/lib/hyperion_test/kim/matcher_spec.rb
|
304
|
+
- spec/lib/hyperion_test/kim/matchers_spec.rb
|
305
|
+
- spec/lib/hyperion_test/kim_spec.rb
|
297
306
|
- spec/lib/types_spec.rb
|
298
307
|
- spec/spec_helper.rb
|
299
308
|
- spec/support/core_helpers.rb
|
300
|
-
has_rdoc:
|
data/lib/hyperion/aux/version.rb
DELETED
@@ -1,74 +0,0 @@
|
|
1
|
-
require 'hyperion/headers'
|
2
|
-
|
3
|
-
class Hyperion
|
4
|
-
class FakeServer
|
5
|
-
class Dispatcher
|
6
|
-
# Directs an incoming request to the correct handler
|
7
|
-
|
8
|
-
include Hyperion::Formats
|
9
|
-
include Hyperion::Headers
|
10
|
-
|
11
|
-
def initialize(rules)
|
12
|
-
@rules = rules
|
13
|
-
end
|
14
|
-
|
15
|
-
def dispatch(mimic_route, request)
|
16
|
-
rule = find_matching_rule(mimic_route, request)
|
17
|
-
rule or return [404, {}, "Not stubbed: #{mimic_route.inspect} #{request.env}"]
|
18
|
-
request = make_req_obj(request.body.read, request.env['CONTENT_TYPE'])
|
19
|
-
response = rule.handler.call(request)
|
20
|
-
if rack_response?(response)
|
21
|
-
code, headers, body = *response
|
22
|
-
[code, headers, write(body, :json)]
|
23
|
-
else
|
24
|
-
if rule.rest_route
|
25
|
-
rd = rule.rest_route.response_descriptor
|
26
|
-
[200, {'Content-Type' => content_type_for(rd)}, write(response, rd)]
|
27
|
-
else
|
28
|
-
# better to return a 500 than raise an error, since we're executing in the forked server.
|
29
|
-
[500, {}, "An 'allow' block must return a rack-style response if it was not passed a RestRoute"]
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
attr_reader :rules
|
37
|
-
|
38
|
-
def rack_response?(x)
|
39
|
-
x.is_a?(Array) && x.size == 3 && x.first.is_a?(Integer) && x.drop(1).any?{|y| !y.is_a?(Integer)}
|
40
|
-
end
|
41
|
-
|
42
|
-
def find_matching_rule(mimic_route, request)
|
43
|
-
matching_rules = rules.select{|rule| rule.mimic_route == mimic_route}
|
44
|
-
matching_rules.reverse.detect{|rule| headers_match?(rule.headers, request.env)}
|
45
|
-
# reverse so that if there are duplicates, the last one wins
|
46
|
-
end
|
47
|
-
|
48
|
-
def make_req_obj(raw_body, content_type)
|
49
|
-
body = raw_body.empty? ? '' : read(raw_body, format_for(content_type))
|
50
|
-
Request.new(body)
|
51
|
-
end
|
52
|
-
|
53
|
-
def headers_match?(rule_headers, actual_headers)
|
54
|
-
sinatrize_headers(rule_headers).subhash?(actual_headers)
|
55
|
-
end
|
56
|
-
|
57
|
-
def sinatrize_headers(headers)
|
58
|
-
headers.map{|k, v| [sinatra_header(k), v]}.to_h
|
59
|
-
end
|
60
|
-
|
61
|
-
def sinatra_header(header)
|
62
|
-
# TODO: there should be a function in Sinatra that does this already
|
63
|
-
cased_header = header.upcase.gsub('-', '_')
|
64
|
-
case cased_header
|
65
|
-
when 'ACCEPT' then 'HTTP_ACCEPT'
|
66
|
-
when 'EXPECT' then 'HTTP_EXPECT'
|
67
|
-
when 'HOST' then 'HTTP_HOST'
|
68
|
-
when 'USER_AGENT' then 'HTTP_USER_AGENT'
|
69
|
-
else cased_header
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|