right_develop 2.1.5 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. data/VERSION +1 -1
  2. data/bin/right_develop +4 -0
  3. data/lib/right_develop.rb +1 -0
  4. data/lib/right_develop/commands.rb +2 -1
  5. data/lib/right_develop/commands/server.rb +194 -0
  6. data/lib/right_develop/testing.rb +31 -0
  7. data/lib/right_develop/testing/clients.rb +36 -0
  8. data/lib/right_develop/testing/clients/rest.rb +34 -0
  9. data/lib/right_develop/testing/clients/rest/requests.rb +38 -0
  10. data/lib/right_develop/testing/clients/rest/requests/base.rb +305 -0
  11. data/lib/right_develop/testing/clients/rest/requests/playback.rb +293 -0
  12. data/lib/right_develop/testing/clients/rest/requests/record.rb +175 -0
  13. data/lib/right_develop/testing/recording.rb +33 -0
  14. data/lib/right_develop/testing/recording/config.rb +571 -0
  15. data/lib/right_develop/testing/recording/metadata.rb +805 -0
  16. data/lib/right_develop/testing/servers/might_api/.gitignore +3 -0
  17. data/lib/right_develop/testing/servers/might_api/Gemfile +6 -0
  18. data/lib/right_develop/testing/servers/might_api/Gemfile.lock +18 -0
  19. data/lib/right_develop/testing/servers/might_api/app/base.rb +323 -0
  20. data/lib/right_develop/testing/servers/might_api/app/echo.rb +73 -0
  21. data/lib/right_develop/testing/servers/might_api/app/playback.rb +46 -0
  22. data/lib/right_develop/testing/servers/might_api/app/record.rb +45 -0
  23. data/lib/right_develop/testing/servers/might_api/config.ru +8 -0
  24. data/lib/right_develop/testing/servers/might_api/config/init.rb +47 -0
  25. data/lib/right_develop/testing/servers/might_api/lib/config.rb +204 -0
  26. data/lib/right_develop/testing/servers/might_api/lib/logger.rb +68 -0
  27. data/right_develop.gemspec +30 -2
  28. metadata +84 -34
@@ -0,0 +1,3 @@
1
+ config/might_deploy.yml
2
+ fixtures/
3
+ *.log
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'extlib'
4
+ gem 'rack'
5
+ gem 'rest-client'
6
+ gem 'right_support'
@@ -0,0 +1,18 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ extlib (0.9.16)
5
+ mime-types (2.2)
6
+ rack (1.5.2)
7
+ rest-client (1.6.7)
8
+ mime-types (>= 1.16)
9
+ right_support (2.8.20)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ extlib
16
+ rack
17
+ rest-client
18
+ right_support
@@ -0,0 +1,323 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require ::File.expand_path('../../config/init', __FILE__)
24
+
25
+ require 'rack/chunked'
26
+ require 'stringio'
27
+ require 'uri'
28
+
29
+ module RightDevelop::Testing::Server::MightApi
30
+ module App
31
+ class Base
32
+
33
+ MAX_REDIRECTS = 10 # 500 after so many redirects
34
+
35
+ # exceptions
36
+ class MightError < StandardError; end
37
+ class MissingRoute < MightError; end
38
+
39
+ attr_reader :config, :logger, :state_file_path
40
+
41
+ def initialize(state_file_name)
42
+ @config = ::RightDevelop::Testing::Server::MightApi::Config
43
+ @logger = ::RightDevelop::Testing::Server::MightApi.logger
44
+
45
+ @state_file_path = state_file_name ? ::File.join(@config.fixtures_dir, state_file_name) : nil
46
+ end
47
+
48
+ def call(env)
49
+ env['rack.logger'] ||= logger
50
+
51
+ # read body from stream.
52
+ request = ::Rack::Request.new(env)
53
+ body = request.body.read
54
+
55
+ # proxy any headers from env starting with HTTP_
56
+ headers = env.inject({}) do |r, (k,v)|
57
+ # note that HTTP_HOST refers to this proxy server instead of the
58
+ # proxied target server. in the case of AWS authentication, it is
59
+ # necessary to pass the value through unmodified or else AWS auth
60
+ # fails.
61
+ if k.start_with?('HTTP_')
62
+ r[k[5..-1]] = v
63
+ end
64
+ r
65
+ end
66
+
67
+ # special cases.
68
+ ['ACCEPT', 'CONTENT_TYPE', 'CONTENT_LENGTH', 'USER_AGENT'].each do |key|
69
+ headers[key] = env[key] unless env[key].to_s.empty?
70
+ end
71
+
72
+ # handler
73
+ verb = request.request_method
74
+ uri = ::URI.parse(request.url)
75
+ handle_request(env, verb, uri, headers, body)
76
+ rescue MissingRoute => e
77
+ message = "#{e.class} #{e.message}"
78
+ logger.error(message)
79
+ if config.routes.empty?
80
+ logger.error("No routes configured.")
81
+ else
82
+ logger.error("The following routes are configured:")
83
+ config.routes.keys.each do |prefix|
84
+ logger.error(" #{prefix}...")
85
+ end
86
+ end
87
+
88
+ # not a 404 because this is a proxy/stub service and 40x might appear to
89
+ # have come from a proxied request/response whereas 500 is never an
90
+ # expected response.
91
+ internal_server_error(message)
92
+ rescue ::RightDevelop::Testing::Client::Rest::Request::Playback::PlaybackError => e
93
+ # response has not been recorded.
94
+ message = e.message
95
+ trace = [e.class.name] + (e.backtrace || [])
96
+ logger.error(message)
97
+ logger.debug(trace.join("\n"))
98
+ internal_server_error(message)
99
+ rescue ::Exception => e
100
+ message = "Unhandled exception: #{e.class} #{e.message}"
101
+ trace = e.backtrace || []
102
+ if logger
103
+ logger.error(message)
104
+ logger.debug(trace.join("\n"))
105
+ else
106
+ env['rack.errors'].puts(message)
107
+ env['rack.errors'].puts(trace.join("\n"))
108
+ end
109
+ internal_server_error(message)
110
+ end
111
+
112
+ # Handler.
113
+ #
114
+ # @param [Hash] env from rack
115
+ # @param [String] verb as one of ['GET', 'POST', etc.]
116
+ # @param [URI] uri parsed from full url
117
+ # @param [Hash] headers for proxy call with any non-proxy data omitted
118
+ # @param [String] body streamed from payload or empty
119
+ #
120
+ # @return [TrueClass] always true
121
+ def handle_request(env, verb, uri, headers, body)
122
+ raise ::NotImplementedError, 'Must be overridden'
123
+ end
124
+
125
+ # Makes a proxied API request using the given request class.
126
+ #
127
+ # @param [Class] request_class for API call
128
+ # @param [String] verb as one of ['GET', 'POST', etc.]
129
+ # @param [URI] uri parsed from full url
130
+ # @param [Hash] headers for proxy call with any non-proxy data omitted
131
+ # @param [String] body streamed from payload or empty
132
+ # @param [Integer] throttle for playback or nil
133
+ #
134
+ # @return [Array] rack-style tuple of [code, headers, [body]]
135
+ def proxy(request_class, verb, uri, headers, body, throttle = nil)
136
+
137
+ # check routes.
138
+ unless route = find_route(uri)
139
+ raise MissingRoute, "No route configured for #{uri.path.inspect}"
140
+ end
141
+ route_path, route_data = route
142
+ response = nil
143
+ max_redirects = MAX_REDIRECTS
144
+ while response.nil? do
145
+ request_proxy = nil
146
+ begin
147
+ proxied_url = ::File.join(route_data[:url], uri.path)
148
+ unless uri.query.to_s.empty?
149
+ proxied_url << '?' << uri.query
150
+ end
151
+ proxied_headers = proxy_headers(headers, route_data)
152
+
153
+ request_options = {
154
+ fixtures_dir: config.fixtures_dir,
155
+ logger: logger,
156
+ route_path: route_path,
157
+ route_data: route_data,
158
+ state_file_path: state_file_path,
159
+ method: verb.downcase.to_sym,
160
+ url: proxied_url,
161
+ headers: proxied_headers,
162
+ payload: body
163
+ }
164
+ request_options[:throttle] = throttle if throttle
165
+ request_proxy = request_class.new(request_options)
166
+
167
+ # log normalized data for obfuscation.
168
+ logger.debug("normalized request headers = #{request_proxy.request_metadata.headers.inspect}")
169
+ logger.debug("normalized request body:\n" << request_proxy.request_metadata.body)
170
+
171
+ request_proxy.execute do |rest_response, rest_request, net_http_response, &block|
172
+
173
+ # headers.
174
+ response_headers = normalize_rack_response_headers(net_http_response.to_hash)
175
+
176
+ # eliminate headers that interfere with response via proxy.
177
+ %w(
178
+ connection status content-encoding
179
+ ).each { |key| response_headers.delete(key) }
180
+
181
+ case response_code = Integer(rest_response.code)
182
+ when 301, 302, 307
183
+ raise RestClient::Exceptions::EXCEPTIONS_MAP[response_code].new(rest_response, response_code)
184
+ else
185
+ # special handling for chunked body.
186
+ if response_headers['transfer-encoding'] == 'chunked'
187
+ response_body = ::Rack::Chunked::Body.new([rest_response.body])
188
+ else
189
+ response_body = [rest_response.body]
190
+ end
191
+ response = [response_code, response_headers, response_body]
192
+ end
193
+ end
194
+
195
+ # log normalized data for obfuscation.
196
+ logger.debug("normalized response headers = #{request_proxy.response_metadata.headers.inspect}")
197
+ logger.debug("normalized response body:\n" << request_proxy.response_metadata.body.to_s)
198
+ rescue RestClient::RequestTimeout
199
+ net_http_response = request_proxy.handle_timeout
200
+ response_code = Integer(net_http_response.code)
201
+ response_headers = normalize_rack_response_headers(net_http_response.to_hash)
202
+ response_body = [net_http_response.body]
203
+ response = [response_code, response_headers, response_body]
204
+ rescue RestClient::Exception => e
205
+ case e.http_code
206
+ when 301, 302, 307
207
+ max_redirects -= 1
208
+ raise MightError.new('Exceeded max redirects') if max_redirects < 0
209
+ if location = e.response.headers[:location]
210
+ redirect_uri = ::URI.parse(location)
211
+ redirect_uri.path = ''
212
+ redirect_uri.query = nil
213
+ logger.debug("#{e.message} from #{route_data[:url]} to #{redirect_uri}")
214
+ route_data[:url] = redirect_uri.to_s
215
+
216
+ # move to end of FIFO queue for retry.
217
+ request_proxy.forget_outstanding_request
218
+ else
219
+ logger.debug("#{e.message} was missing expected location header.")
220
+ raise
221
+ end
222
+ else
223
+ raise
224
+ end
225
+ ensure
226
+ # remove from FIFO queue in case of any unhandled error.
227
+ request_proxy.forget_outstanding_request if request_proxy
228
+ end
229
+ end
230
+ response
231
+ end
232
+
233
+ # @param [URI] uri path to find
234
+ #
235
+ # @return [Array] pair of [prefix, data] or nil
236
+ def find_route(uri)
237
+ find_path = uri.path
238
+ logger.debug "Route URI path to match = #{find_path.inspect}"
239
+ config.routes.find do |prefix, data|
240
+ matched = find_path.start_with?(prefix)
241
+ logger.debug "Tried = #{prefix.inspect}, matched = #{matched}"
242
+ matched
243
+ end
244
+ end
245
+
246
+ # Sets the header style using configuration of the proxied service.
247
+ #
248
+ # @param [Hash] headers for proxy
249
+ # @param [Hash] route_data containing header configuration, if any
250
+ #
251
+ # @return [Mash] proxied headers
252
+ def proxy_headers(headers, route_data)
253
+ proxied = nil
254
+ if proxy_data = route_data[:proxy]
255
+ if header_data = proxy_data[:header]
256
+ to_separator = (header_data[:separator] == :underscore) ? '_' : '-'
257
+ from_separator = (to_separator == '-') ? '_' : '-'
258
+ proxied = headers.inject(::Mash.new) do |h, (k, v)|
259
+ k = k.to_s
260
+ case header_data[:case]
261
+ when nil
262
+ k = k.gsub(from_separator, to_separator)
263
+ when :lower
264
+ k = k.downcase.gsub(from_separator, to_separator)
265
+ when :upper
266
+ k = k.upcase.gsub(from_separator, to_separator)
267
+ when :capitalize
268
+ k = k.split(/-|_/).map { |word| word.capitalize }.join(to_separator)
269
+ else
270
+ raise ::ArgumentError,
271
+ "Unexpected header case: #{route_data.inspect}"
272
+ end
273
+ h[k] = v
274
+ h
275
+ end
276
+ end
277
+ end
278
+ proxied || ::Mash.new(headers)
279
+ end
280
+
281
+ # rack has a convention of newline-delimited header multi-values.
282
+ #
283
+ # HACK: changes underscore to dash to defeat RestClient::AbstractResponse
284
+ # line 27 (on client side) from failing to parse cookies array; it
285
+ # incorrectly calls .inject on the stringized form instead of using the
286
+ # raw array form or parsing the cookies into a hash, but only if the raw
287
+ # name is 'set_cookie' ('set-cookie' is okay).
288
+ #
289
+ # even wierder, on line 78 it assumes the raw name is 'set-cookie' and
290
+ # that works out for us here.
291
+ #
292
+ # @param [Hash] headers to normalize
293
+ #
294
+ # @return [Hash] normalized headers
295
+ def normalize_rack_response_headers(headers)
296
+ headers.inject({}) do |h, (k, v)|
297
+ h[k.to_s.gsub('_', '-').downcase] = v.join("\n")
298
+ h
299
+ end
300
+ end
301
+
302
+ # @return [Array] rack-style response for 500
303
+ def internal_server_error(message)
304
+ formal = <<EOF
305
+ MightAPI internal error
306
+
307
+ Problem:
308
+ #{message}
309
+ EOF
310
+
311
+ [
312
+ 500,
313
+ {
314
+ 'Content-Type' => 'text/plain',
315
+ 'Content-Length' => ::Rack::Utils.bytesize(formal).to_s
316
+ },
317
+ [formal]
318
+ ]
319
+ end
320
+
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,73 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require ::File.expand_path('../base', __FILE__)
24
+
25
+ require 'right_support'
26
+
27
+ module RightDevelop::Testing::Server::MightApi::App
28
+
29
+ # Implements an echo service.
30
+ class Echo < ::RightDevelop::Testing::Server::MightApi::App::Base
31
+
32
+ # metadata
33
+ METADATA_CLASS = ::RightDevelop::Testing::Recording::Metadata
34
+
35
+ def initialize
36
+ super(nil)
37
+ end
38
+
39
+ # @see RightDevelop::Testing::Server::MightApi::App::Base#handle_request
40
+ def handle_request(env, verb, uri, headers, body)
41
+
42
+ # check routes.
43
+ response = ::Rack::Response.new
44
+ if route = find_route(uri)
45
+ response.write "URL matched #{route.last[:name].inspect}\n"
46
+ else
47
+ response.write "Failed to match any route. The following routes are valid:\n"
48
+ config.routes.keys.each do |prefix|
49
+ response.write "Prefix = #{prefix.inspect}\n\n"
50
+ end
51
+ end
52
+
53
+ # echo request back as response.
54
+ response.write "\nruby %sp%s\n\n" % [RUBY_VERSION, RUBY_PATCHLEVEL]
55
+ response.write "Raw configuration = #{::JSON.pretty_generate(config.to_hash)}\n\n" # hash order is significant in config
56
+ config.routes.each do |route_path, route_config|
57
+ response.write "=== Compiled route matchers begin for root = #{route_path.inspect}:\nmatchers = {\n"
58
+ (route_config[METADATA_CLASS::MATCHERS_KEY] || {}).each do |regex, route_data|
59
+ response.write " #{regex.inspect} =>\n #{route_data.inspect},\n"
60
+ end
61
+ response.write "}\n=== Compiled route matchers end.\n\n"
62
+ end
63
+ response.write "env = #{METADATA_CLASS.deep_sorted_json(env, true)}\n\n"
64
+ response.write "ENV = #{METADATA_CLASS.deep_sorted_json(::ENV, true)}\n\n"
65
+ response.write "verb = #{verb.inspect}\n\n"
66
+ response.write "uri = #{uri}\n\n"
67
+ response.write "headers = #{METADATA_CLASS.deep_sorted_json(headers, true)}\n\n"
68
+ response.write "body = #{body.inspect}\n\n"
69
+ response.finish
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,46 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require ::File.expand_path('../base', __FILE__)
24
+
25
+ module RightDevelop::Testing::Server::MightApi::App
26
+ class Playback < ::RightDevelop::Testing::Server::MightApi::App::Base
27
+
28
+ STATE_FILE_NAME = 'playback_state.yml'
29
+
30
+ def initialize
31
+ super(STATE_FILE_NAME)
32
+ end
33
+
34
+ # @see RightDevelop::Testing::Server::MightApi::App::Base#handle_request
35
+ def handle_request(env, verb, uri, headers, body)
36
+ proxy(
37
+ ::RightDevelop::Testing::Client::Rest::Request::Playback,
38
+ verb,
39
+ uri,
40
+ headers,
41
+ body,
42
+ config.throttle)
43
+ end
44
+
45
+ end
46
+ end