rack-test_app 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8f2ce0590323aed326f765838e0e0ff9fcbafa23
4
+ data.tar.gz: ac91d827a933137c4b1a44b15d453f64f3d4c094
5
+ SHA512:
6
+ metadata.gz: 084c7935751936114289ab16209e1b2e537d9acd4945ad34790ee611fc5cf8a24ef0e0efa6909fe01a0d3c3f2baaed3cc47cb585b51c2378fae373320a58fb85
7
+ data.tar.gz: 3982adf894d08f028bded159fe9f6e30ee9a3142a0e21cad104402e88b09abf9fc8f18b39f741075a79b5852db877fb3473d56ca428c6d0ec1149e160521a987
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,122 @@
1
+ # Rack::TestApp
2
+
3
+
4
+ Rack::TestApp is another testing helper library for Rack application.
5
+ IMO, it is more intuitive than Rack::Test.
6
+
7
+ Rack::TestApp requires Ruby >= 2.0.
8
+
9
+
10
+ ## Installation
11
+
12
+ ```console
13
+ $ gem install rack-test_app
14
+ ```
15
+
16
+ Or:
17
+
18
+ ```console
19
+ $ echo "gem 'rack-test_app'" >> Gemfile
20
+ $ bundle
21
+ ```
22
+
23
+
24
+ ## Example
25
+
26
+ ```ruby
27
+ require 'rack'
28
+ require 'rack/lint'
29
+ require 'rack/test_app'
30
+
31
+ ## sample Rack app
32
+ app = proc {|env|
33
+ text = "{\"status\":\"OK\"}"
34
+ headers = {"Content-Type" => "application/json",
35
+ "Content-Length" => text.bytesize.to_s}
36
+ [200, headers, [text]]
37
+ }
38
+
39
+ ## crate wrapper objects
40
+ http = Rack::TestApp.wrap(Rack::Lint.new(app))
41
+ https = Rack::TestApp.wrap(Rack::Lint.new(app), env: {'HTTPS'=>'on'})
42
+
43
+ ## simulates http request
44
+ result = http.GET('/api/hello', query: {'name'=>'World'})
45
+ # or http.get(...) if you like.
46
+
47
+ ## test result
48
+ r = result
49
+ assert_equal 200, r.status
50
+ assert_equal "application/json", r.headers['Content-Type']
51
+ assert_equal "application/json", r.content_type
52
+ assert_equal 15, r.content_length
53
+ assert_equal ({"status"=>"OK"}), r.body_json
54
+ assert_equal "{\"status\":\"OK\"}", r.body_text
55
+ assert_equal "{\"status\":\"OK\"}", r.body_binary # encoing: ASCII-8BIT
56
+ assert_equal nil, r.location
57
+
58
+ ## (experimental) confirm environ object (if you want)
59
+ #p http.last_env
60
+ ```
61
+
62
+ * You can call `http.get()`/`http.post()` instead of `http.GET()`/`http.POST()`
63
+ if you prefer.
64
+ * `http.last_env` is an experimental feature (may be dropped in the future).
65
+
66
+
67
+ ## More Examples
68
+
69
+ ```ruby
70
+ ## query string
71
+ r = http.GET('/api/hello', query: 'name=World')
72
+ r = http.GET('/api/hello', query: {'name'=>'World'})
73
+
74
+ ## form parameters
75
+ r = http.POST('/api/hello', form: 'name=World')
76
+ r = http.POST('/api/hello', form: {'name'=>'World'})
77
+
78
+ ## json
79
+ r = http.POST('/api/hello', json: {'name'=>'World'})
80
+
81
+ ## multipart
82
+ mp = {
83
+ "name1" => "value1",
84
+ "file1" => File.open("data/example1.jpg", 'rb'),
85
+ }
86
+ r = http.POST('/api/hello', multipart: mp)
87
+
88
+ ## multipart #2
89
+ boundary = "abcdefg1234567" # or nil
90
+ mp = Rack::TestApp::MultipartBuilder.new(boundary)
91
+ mp.add("name1", "value1")
92
+ mp.add("file1", File.read('data/example1.jpg'), "example1.jpg", "image/jpeg")
93
+ r = http.POST('/api/hello', multipart: mp)
94
+
95
+ ## input
96
+ r = http.POST('/api/hello', input: "x=1&y=2&z=3")
97
+
98
+ ## headers
99
+ r = http.GET('/api/hello', headers: {"X-Requested-With"=>"XMLHttpRequest"})
100
+
101
+ ## cookies
102
+ r = http.GET('/api/hello', cookies: "name1=value1")
103
+ r = http.GET('/api/hello', cookies: {"name1"=>"value1"})
104
+ r = http.GET('/api/hello', cookies: {"name1"=>{:name=>'name1', :value=>'value1'}})
105
+
106
+ ## cookies #2
107
+ r1 = http.POST('/api/login')
108
+ r2 = http.GET('/api/hello', cookies: r1.cookies)
109
+ http.with(cookies: r1.cookies, headers: {}) do |http_|
110
+ r3 = http_.GET('/api/hello')
111
+ end
112
+
113
+ ## env
114
+ r = http.GET('/api/hello', env: {"HTTPS"=>"on"})
115
+ ```
116
+
117
+
118
+ ## Copyright and License
119
+
120
+ $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
121
+
122
+ $License: MIT-LICENSE $
@@ -0,0 +1,49 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
11
+
12
+
13
+ desc "show how to release"
14
+ task :help do
15
+ puts <<END
16
+ How to release:
17
+
18
+ $ git checkout dev
19
+ $ git diff
20
+ $ which ruby
21
+ $ rake test # for confirmation
22
+ $ git checkout -b rel-1.0 # or git checkout rel-1.0
23
+ $ rake edit rel=1.0.0
24
+ $ git diff
25
+ $ git commit -a -m "release preparation for 1.0.0"
26
+ $ rake build # for confirmation
27
+ $ rake install # for confirmation
28
+ $ rake release
29
+ $ git push -u --tags origin rel-1.0
30
+ END
31
+
32
+ end
33
+
34
+
35
+ desc "edit files (for release preparation)"
36
+ task :edit do
37
+ rel = ENV['rel'] or
38
+ raise "ERROR: 'rel' environment variable expected."
39
+ filenames = Dir[*%w[lib/**/*.rb test/**/*_test.rb]]
40
+ filenames.each do |fname|
41
+ File.open(fname, 'r+', encoding: 'utf-8') do |f|
42
+ content = f.read()
43
+ x = content.gsub!(/\$Release:.*?\$/, "$Release: #{rel} $")
44
+ f.rewind()
45
+ f.truncate(0)
46
+ f.write(content)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,487 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### $Release: 1.0.0 $
5
+ ### $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
6
+ ### $License: MIT License $
7
+ ###
8
+
9
+
10
+ require 'json'
11
+ require 'uri'
12
+ require 'stringio'
13
+ require 'digest/sha1'
14
+
15
+ require 'rack'
16
+
17
+
18
+ module Rack
19
+
20
+
21
+ module TestApp
22
+
23
+ VERSION = '$Release: 1.0.0 $'.split()[1]
24
+
25
+
26
+ module Util
27
+
28
+ module_function
29
+
30
+ def percent_encode(str)
31
+ #; [!a96jo] encodes string into percent encoding format.
32
+ return URI.encode_www_form_component(str)
33
+ end
34
+
35
+ def percent_decode(str)
36
+ #; [!kl9sk] decodes percent encoded string.
37
+ return URI.decode_www_form_component(str)
38
+ end
39
+
40
+ def build_query_string(query) # :nodoc:
41
+ #; [!098ac] returns nil when argument is nil.
42
+ #; [!z9ds2] returns argument itself when it is a string.
43
+ #; [!m5yyh] returns query string when Hash or Array passed.
44
+ #; [!nksh3] raises ArgumentError when passed value except nil, string, hash or array.
45
+ case query
46
+ when nil ; return nil
47
+ when String ; return query
48
+ when Hash, Array
49
+ return query.collect {|k, v| "#{percent_encode(k.to_s)}=#{percent_encode(v.to_s)}" }.join('&')
50
+ else
51
+ raise ArgumentError.new("Hash or Array expected but got #{query.inspect}.")
52
+ end
53
+ end
54
+
55
+ COOKIE_KEYS = {
56
+ 'path' => :path,
57
+ 'domain' => :domain,
58
+ 'expires' => :expires,
59
+ 'max-age' => :max_age,
60
+ 'httponly' => :httponly,
61
+ 'secure' => :secure,
62
+ }
63
+
64
+ def parse_set_cookie(set_cookie_value)
65
+ #; [!hvvu4] parses 'Set-Cookie' header value and returns hash object.
66
+ keys = COOKIE_KEYS
67
+ d = {}
68
+ set_cookie_value.split(/;\s*/).each do |string|
69
+ #; [!h75uc] sets true when value is missing such as 'secure' or 'httponly' attribute.
70
+ k, v = string.strip().split('=', 2)
71
+ #
72
+ if d.empty?
73
+ d[:name] = k
74
+ d[:value] = v
75
+ elsif (sym = keys[k.downcase])
76
+ #; [!q1h29] sets true as value for Secure or HttpOnly attribute.
77
+ #; [!50iko] raises error when attribute value specified for Secure or HttpOnly attirbute.
78
+ if sym == :secure || sym == :httponly
79
+ v.nil? or
80
+ raise TypeError.new("#{k}=#{v}: unexpected attribute value.")
81
+ v = true
82
+ #; [!sucrm] raises error when attribute value is missing when neighter Secure nor HttpOnly.
83
+ else
84
+ ! v.nil? or
85
+ raise TypeError.new("#{k}: attribute value expected but not specified.")
86
+ #; [!f3rk7] converts string into integer for Max-Age attribute.
87
+ #; [!wgzyz] raises error when Max-Age attribute value is not a positive integer.
88
+ if sym == :max_age
89
+ v =~ /\A\d+\z/ or
90
+ raise TypeError.new("#{k}=#{v}: positive integer expected.")
91
+ v = v.to_i
92
+ end
93
+ end
94
+ d[sym] = v
95
+ #; [!8xg63] raises ArgumentError when unknown attribute exists.
96
+ else
97
+ raise TypeError.new("#{k}=#{v}: unknown cookie attribute.")
98
+ end
99
+ end
100
+ return d
101
+ end
102
+
103
+ def randstr_b64()
104
+ #; [!yq0gv] returns random string, encoded with urlsafe base64.
105
+ ## Don't use SecureRandom; entropy of /dev/random or /dev/urandom
106
+ ## should be left for more secure-sensitive purpose.
107
+ s = "#{rand()}#{rand()}#{rand()}#{Time.now.to_f}"
108
+ binary = ::Digest::SHA1.digest(s)
109
+ return [binary].pack('m').chomp("=\n").tr('+/', '-_')
110
+ end
111
+
112
+ def guess_content_type(filename, default='application/octet-stream')
113
+ #; [!xw0js] returns content type guessed from filename.
114
+ #; [!dku5c] returns 'application/octet-stream' when failed to guess content type.
115
+ ext = ::File.extname(filename)
116
+ return Rack::Mime.mime_type(ext, default)
117
+ end
118
+
119
+ end
120
+
121
+
122
+ class MultipartBuilder
123
+
124
+ def initialize(boundary=nil)
125
+ #; [!ajfgl] sets random string as boundary when boundary is nil.
126
+ @boundary = boundary || Util.randstr_b64()
127
+ @params = []
128
+ end
129
+
130
+ attr_reader :boundary
131
+
132
+ def add(name, value, filename=nil, content_type=nil)
133
+ #; [!tp4bk] detects content type from filename when filename is not nil.
134
+ content_type ||= Util.guess_content_type(filename) if filename
135
+ @params << [name, value, filename, content_type]
136
+ self
137
+ end
138
+
139
+ def add_file(name, file, content_type=nil)
140
+ #; [!uafqa] detects content type from filename when content type is not provided.
141
+ content_type ||= Util.guess_content_type(file.path)
142
+ #; [!b5811] reads file content and adds it as param value.
143
+ add(name, file.read(), ::File.basename(file.path), content_type)
144
+ #; [!36bsu] closes opened file automatically.
145
+ file.close()
146
+ self
147
+ end
148
+
149
+ def to_s
150
+ #; [!61gc4] returns multipart form string.
151
+ boundary = @boundary
152
+ s = "".force_encoding('ASCII-8BIT')
153
+ @params.each do |name, value, filename, content_type|
154
+ s << "--#{boundary}\r\n"
155
+ if filename
156
+ s << "Content-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\"\r\n"
157
+ else
158
+ s << "Content-Disposition: form-data; name=\"#{name}\"\r\n"
159
+ end
160
+ s << "Content-Type: #{content_type}\r\n" if content_type
161
+ s << "\r\n"
162
+ s << value.force_encoding('ASCII-8BIT')
163
+ s << "\r\n"
164
+ end
165
+ s << "--#{boundary}--\r\n"
166
+ return s
167
+ end
168
+
169
+ end
170
+
171
+
172
+ ##
173
+ ## Builds environ hash object.
174
+ ##
175
+ ## ex:
176
+ ## json = {"x"=>1, "y"=>2}
177
+ ## env = Rack::TestApp.new_env(:POST, '/api/entry?x=1', json: json)
178
+ ## p env['REQUEST_METHOD'] #=> 'POST'
179
+ ## p env['PATH_INFO'] #=> '/api/entry'
180
+ ## p env['QUERY_STRING'] #=> 'x=1'
181
+ ## p env['CONTENT_TYPE'] #=> 'application/json'
182
+ ## p JSON.parse(env['rack.input'].read()) #=> {"x"=>1, "y"=>2}
183
+ ##
184
+ def self.new_env(meth=:GET, path="/", query: nil, form: nil, multipart: nil, json: nil, input: nil, headers: nil, cookies: nil, env: nil)
185
+ #uri = "http://localhost:80#{path}"
186
+ #opts["REQUEST_METHOD"] = meth
187
+ #env = Rack::MockRequest.env_for(uri, opts)
188
+ #
189
+ #; [!j879z] sets 'HTTPS' with 'on' when 'rack.url_scheme' is 'https'.
190
+ #; [!vpwvu] sets 'HTTPS' with 'on' when 'HTTPS' is 'on'.
191
+ https = env && (env['rack.url_scheme'] == 'https' || env['HTTPS'] == 'on')
192
+ #
193
+ err = proc {|a, b|
194
+ ArgumentError.new("new_env(): not allowed both '#{a}' and '#{b}' at a time.")
195
+ }
196
+ ctype = nil
197
+ #; [!2uvyb] raises ArgumentError when both query string and 'query' kwarg specified.
198
+ if query
199
+ arr = path.split('?', 2)
200
+ arr.length != 2 or
201
+ raise ArgumentError.new("new_env(): not allowed both query string and 'query' kwarg at a time.")
202
+ #; [!8tq3m] accepts query string in path string.
203
+ else
204
+ path, query = path.split('?', 2)
205
+ end
206
+ #; [!d1c83] when 'form' kwarg specified...
207
+ if form
208
+ #; [!c779l] raises ArgumentError when both 'form' and 'json' are specified.
209
+ ! json or raise err.call('form', 'json')
210
+ input = Util.build_query_string(form)
211
+ #; [!5iv35] sets content type with 'application/x-www-form-urlencoded'.
212
+ ctype = "application/x-www-form-urlencoded"
213
+ end
214
+ #; [!prv5z] when 'json' kwarg specified...
215
+ if json
216
+ #; [!2o0ph] raises ArgumentError when both 'json' and 'multipart' are specified.
217
+ ! multipart or raise err.call('json', 'multipart')
218
+ input = json.is_a?(String) ? json : JSON.dump(json)
219
+ #; [!ta24a] sets content type with 'application/json'.
220
+ ctype = "application/json"
221
+ end
222
+ #; [!dnvgj] when 'multipart' kwarg specified...
223
+ if multipart
224
+ #; [!b1d1t] raises ArgumentError when both 'multipart' and 'form' are specified.
225
+ ! form or raise err.call('multipart', 'form')
226
+ #; [!gko8g] 'multipart:' kwarg accepts Hash object (which is converted into multipart data).
227
+ if multipart.is_a?(Hash)
228
+ dict = multipart
229
+ multipart = dict.each_with_object(MultipartBuilder.new) do |(k, v), mp|
230
+ v.is_a?(::File) ? mp.add_file(k, v) : mp.add(k, v.to_s)
231
+ end
232
+ end
233
+ input = multipart.to_s
234
+ #; [!dq33d] sets content type with 'multipart/form-data'.
235
+ m = /\A--(\S+)\r\n/.match(input) or
236
+ raise ArgumentError.new("invalid multipart format.")
237
+ boundary = $1
238
+ ctype = "multipart/form-data; boundary=#{boundary}"
239
+ end
240
+ #; [!iamrk] uses 'application/x-www-form-urlencoded' as default content type of input.
241
+ if input && ! ctype
242
+ ctype ||= headers['Content-Type'] || headers['content-type'] if headers
243
+ ctype ||= env['CONTENT_TYPE'] if env
244
+ ctype ||= "application/x-www-form-urlencoded"
245
+ end
246
+ #; [!7hfri] converts input string into binary.
247
+ input ||= ""
248
+ input = input.encode('ascii-8bit') if input.encoding != Encoding::ASCII_8BIT
249
+ #; [!r3soc] converts query string into binary.
250
+ query_str = Util.build_query_string(query || "")
251
+ query_str = query_str.encode('ascii-8bit')
252
+ #; [!na9w6] builds environ hash object.
253
+ environ = {
254
+ "rack.version" => Rack::VERSION,
255
+ "rack.input" => StringIO.new(input),
256
+ "rack.errors" => StringIO.new,
257
+ "rack.multithread" => true,
258
+ "rack.multiprocess" => true,
259
+ "rack.run_once" => false,
260
+ "rack.url_scheme" => https ? "https" : "http",
261
+ "REQUEST_METHOD" => meth.to_s,
262
+ "SERVER_NAME" => "localhost",
263
+ "SERVER_PORT" => https ? "443" : "80",
264
+ "QUERY_STRING" => query_str,
265
+ "PATH_INFO" => path,
266
+ "HTTPS" => https ? "on" : "off",
267
+ "SCRIPT_NAME" => "",
268
+ "CONTENT_LENGTH" => (input ? input.bytesize.to_s : "0"),
269
+ "CONTENT_TYPE" => ctype,
270
+ }
271
+ #; [!ezvdn] unsets CONTENT_TYPE when not input.
272
+ environ.delete("CONTENT_TYPE") if input.empty?
273
+ #; [!r4jz8] copies 'headers' kwarg content into environ with 'HTTP_' prefix.
274
+ #; [!ai9t3] don't add 'HTTP_' to Content-Length and Content-Type headers.
275
+ excepts = ['CONTENT_LENGTH', 'CONTENT_TYPE']
276
+ headers.each do |name, value|
277
+ name =~ /\A[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\z/ or
278
+ raise ArgumentError.new("invalid http header name: #{name.inspect}")
279
+ value.is_a?(String) or
280
+ raise ArgumentError.new("http header value should be a string but got: #{value.inspect}")
281
+ ## ex: 'X-Requested-With' -> 'HTTP_X_REQUESTED_WITH'
282
+ k = name.upcase.gsub(/-/, '_')
283
+ k = "HTTP_#{k}" unless excepts.include?(k)
284
+ environ[k] = value
285
+ end if headers
286
+ #; [!a47n9] copies 'env' kwarg content into environ.
287
+ env.each do |name, value|
288
+ case name
289
+ when /\Arack\./
290
+ # ok
291
+ when /\A[A-Z]+(_[A-Z0-9]+)*\z/
292
+ value.is_a?(String) or
293
+ raise ArgumentError.new("rack env value should be a string but got: #{value.inspect}")
294
+ else
295
+ raise ArgumentError.new("invalid rack env key: #{name}")
296
+ end
297
+ environ[name] = value
298
+ end if env
299
+ #; [!pmefk] sets 'HTTP_COOKIE' when 'cookie' kwarg specified.
300
+ if cookies
301
+ s = cookies.is_a?(Hash) ? cookies.map {|k, v|
302
+ #; [!qj7b8] cookie value can be {:name=>'name', :value=>'value'}.
303
+ v = v[:value] if v.is_a?(Hash) && v[:value]
304
+ "#{Util.percent_encode(k)}=#{Util.percent_encode(v)}"
305
+ }.join('; ') : cookies.to_s
306
+ s = "#{environ['HTTP_COOKIE']}; #{s}" if environ['HTTP_COOKIE']
307
+ environ['HTTP_COOKIE'] = s
308
+ end
309
+ #; [!b3ts8] returns environ hash object.
310
+ return environ
311
+ end
312
+
313
+
314
+ class Result
315
+
316
+ def initialize(status, headers, body)
317
+ #; [!3lcsj] accepts response status, headers and body.
318
+ @status = status
319
+ @headers = headers
320
+ @body = body
321
+ #; [!n086q] parses 'Set-Cookie' header.
322
+ @cookies = {}
323
+ raw_str = @headers['Set-Cookie'] || @headers['set-cookie']
324
+ raw_str.split(/\r?\n/).each do |s|
325
+ if s && ! s.empty?
326
+ c = Util.parse_set_cookie(s)
327
+ @cookies[c[:name]] = c
328
+ end
329
+ end if raw_str
330
+ end
331
+
332
+ attr_accessor :status, :headers, :body, :cookies
333
+
334
+ def body_binary
335
+ #; [!mb0i4] returns body as binary string.
336
+ buf = []; @body.each {|x| buf << x }
337
+ s = buf.join()
338
+ @body.close() if @body.respond_to?(:close)
339
+ return s
340
+ end
341
+
342
+ def body_text
343
+ #; [!rr18d] error when 'Content-Type' header is missing.
344
+ ctype = self.content_type or
345
+ raise TypeError.new("body_text(): missing 'Content-Type' header.")
346
+ #; [!dou1n] converts body text according to 'charset' in 'Content-Type' header.
347
+ if ctype =~ /; *charset=(\w[-\w]*)/
348
+ charset = $1
349
+ #; [!cxje7] assumes charset as 'utf-8' when 'Content-Type' is json.
350
+ elsif ctype == "application/json"
351
+ charset = 'utf-8'
352
+ #; [!n4c71] error when non-json 'Content-Type' header has no 'charset'.
353
+ else
354
+ raise TypeError.new("body_text(): missing 'charset' in 'Content-Type' header.")
355
+ end
356
+ #; [!vkj9h] returns body as text string, according to 'charset' in 'Content-Type'.
357
+ return body_binary().force_encoding(charset)
358
+ end
359
+
360
+ def body_json
361
+ #; [!qnic1] returns Hash object representing JSON string.
362
+ return JSON.parse(body_text())
363
+ end
364
+
365
+ def content_type
366
+ #; [!40hcz] returns 'Content-Type' header value.
367
+ return @headers['Content-Type'] || @headers['content-type']
368
+ end
369
+
370
+ def content_length
371
+ #; [!5lb19] returns 'Content-Length' header value as integer.
372
+ #; [!qjktz] returns nil when 'Content-Length' is not set.
373
+ len = @headers['Content-Length'] || @headers['content-length']
374
+ return len ? Integer(len) : len
375
+ end
376
+
377
+ def location
378
+ #; [!8y8lg] returns 'Location' header value.
379
+ return @headers['Location'] || @headers['location']
380
+ end
381
+
382
+ def cookie_value(name)
383
+ #; [!neaf8] returns cookie value if exists.
384
+ #; [!oapns] returns nil if cookie not exists.
385
+ c = @cookies[name]
386
+ return c ? c[:value] : nil
387
+ end
388
+
389
+ end
390
+
391
+
392
+ ##
393
+ ## Wrapper class to test Rack application.
394
+ ## Use 'Rack::TestApp.wrap(app)' instead of 'Rack::TestApp::Wrapper.new(app)'.
395
+ ##
396
+ ## ex:
397
+ ## require 'rack/lint'
398
+ ## require 'rack/test_app'
399
+ ## http = Rack::TestApp.wrap(Rack::Lint.new(app))
400
+ ## https = Rack::TestApp.wrap(Rack::Lint.new(app)), env: {'HTTPS'=>'on'})
401
+ ## resp = http.GET('/api/hello', query: {'name'=>'World'})
402
+ ## assert_equal 200, resp.status
403
+ ## assert_equal "application/json", resp.headers['Content-Type']
404
+ ## assert_equal {"message"=>"Hello World!"}, resp.body_json
405
+ ##
406
+ class Wrapper
407
+
408
+ def initialize(app, env=nil)
409
+ #; [!zz9yg] takes app and optional env objects.
410
+ @app = app
411
+ @env = env
412
+ @last_env = nil
413
+ end
414
+
415
+ attr_reader :last_env
416
+
417
+ ##
418
+ ## helper method to create new wrapper object keeping cookies and headers.
419
+ ##
420
+ ## ex:
421
+ ## http = Rack::TestApp.wrap(Rack::Lint.new(app))
422
+ ## r1 = http.POST('/api/login', form: {user: 'user', password: 'pass'})
423
+ ## http.with(cookies: r1.cookies, headers: {}) do |http_|
424
+ ## r2 = http_.GET('/api/content') # request with r1.cookies
425
+ ## assert_equal 200, r2.status
426
+ ## end
427
+ ##
428
+ def with(headers: nil, cookies: nil, env: nil)
429
+ tmp_env = TestApp.new_env(headers: headers, cookies: cookies, env: env)
430
+ new_env = @env ? @env.dup : {}
431
+ http_headers = tmp_env.each do |k, v|
432
+ new_env[k] = v if k.start_with?('HTTP_')
433
+ end
434
+ new_wrapper = self.class.new(@app, new_env)
435
+ #; [!mkdbu] yields with new wrapper object if block given.
436
+ yield new_wrapper if block_given?
437
+ #; [!0bk12] returns new wrapper object, keeping cookies and headers.
438
+ new_wrapper
439
+ end
440
+
441
+ def request(meth, path, query: nil, form: nil, multipart: nil, json: nil, input: nil, headers: nil, cookies: nil, env: nil)
442
+ #; [!r6sod] merges @env if passed for initializer.
443
+ env = env ? env.merge(@env) : @env if @env
444
+ #; [!4xpwa] creates env object and calls app with it.
445
+ environ = TestApp.new_env(meth, path,
446
+ query: query, form: form, multipart: multipart, json: json,
447
+ input: input, headers: headers, cookies: cookies, env: env)
448
+ @last_env = environ
449
+ tuple = @app.call(environ)
450
+ status, headers, body = tuple
451
+ #; [!eb153] returns Rack::TestApp::Result object.
452
+ return Result.new(status, headers, body)
453
+ end
454
+
455
+ def GET path, kwargs={}; request(:GET , path, kwargs); end
456
+ def POST path, kwargs={}; request(:POST , path, kwargs); end
457
+ def PUT path, kwargs={}; request(:PUT , path, kwargs); end
458
+ def DELETE path, kwargs={}; request(:DELETE , path, kwargs); end
459
+ def HEAD path, kwargs={}; request(:HEAD , path, kwargs); end
460
+ def PATCH path, kwargs={}; request(:PATCH , path, kwargs); end
461
+ def OPTIONS path, kwargs={}; request(:OPTIONS, path, kwargs); end
462
+ def TRACE path, kwargs={}; request(:TRACE , path, kwargs); end
463
+
464
+ ## define aliases because ruby programmer prefers #get() rather than #GET().
465
+ alias get GET
466
+ alias post POST
467
+ alias put PUT
468
+ alias delete DELETE
469
+ alias head HEAD
470
+ alias patch PATCH
471
+ alias options OPTIONS
472
+ alias trace TRACE
473
+
474
+ end
475
+
476
+
477
+ ## Use Rack::TestApp.wrap(app) instead of Rack::TestApp::Wrapper.new(app).
478
+ def self.wrap(app, env=nil)
479
+ #; [!grqlf] creates new Wrapper object.
480
+ return Wrapper.new(app, env)
481
+ end
482
+
483
+
484
+ end
485
+
486
+
487
+ end