hyperion_http 0.1.9 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|