keight 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +263 -0
  4. data/Rakefile +92 -0
  5. data/bench/bench.rb +278 -0
  6. data/bench/benchmarker.rb +502 -0
  7. data/bin/k8rb +496 -0
  8. data/keight.gemspec +36 -0
  9. data/lib/keight/skeleton/.gitignore +10 -0
  10. data/lib/keight/skeleton/app/action.rb +98 -0
  11. data/lib/keight/skeleton/app/api/hello.rb +39 -0
  12. data/lib/keight/skeleton/app/form/.keep +0 -0
  13. data/lib/keight/skeleton/app/helper/.keep +0 -0
  14. data/lib/keight/skeleton/app/model/.keep +0 -0
  15. data/lib/keight/skeleton/app/model.rb +144 -0
  16. data/lib/keight/skeleton/app/page/welcome.rb +17 -0
  17. data/lib/keight/skeleton/app/template/_layout.html.eruby +56 -0
  18. data/lib/keight/skeleton/app/template/welcome.html.eruby +6 -0
  19. data/lib/keight/skeleton/app/usecase/.keep +0 -0
  20. data/lib/keight/skeleton/config/app.rb +29 -0
  21. data/lib/keight/skeleton/config/app_dev.private +11 -0
  22. data/lib/keight/skeleton/config/app_dev.rb +8 -0
  23. data/lib/keight/skeleton/config/app_prod.rb +7 -0
  24. data/lib/keight/skeleton/config/app_stg.rb +5 -0
  25. data/lib/keight/skeleton/config/app_test.private +11 -0
  26. data/lib/keight/skeleton/config/app_test.rb +8 -0
  27. data/lib/keight/skeleton/config/server_puma.rb +22 -0
  28. data/lib/keight/skeleton/config/server_unicorn.rb +21 -0
  29. data/lib/keight/skeleton/config/urlpath_mapping.rb +16 -0
  30. data/lib/keight/skeleton/config.rb +44 -0
  31. data/lib/keight/skeleton/config.ru +21 -0
  32. data/lib/keight/skeleton/index.txt +38 -0
  33. data/lib/keight/skeleton/static/lib/jquery/1.11.3/jquery.min.js +6 -0
  34. data/lib/keight/skeleton/static/lib/jquery/1.11.3/jquery.min.js.gz +0 -0
  35. data/lib/keight/skeleton/static/lib/modernizr/2.8.3/modernizr.min.js +4 -0
  36. data/lib/keight/skeleton/static/lib/modernizr/2.8.3/modernizr.min.js.gz +0 -0
  37. data/lib/keight/skeleton/tmp/upload/.keep +0 -0
  38. data/lib/keight.rb +2017 -0
  39. data/test/data/example1.jpg +0 -0
  40. data/test/data/example1.png +0 -0
  41. data/test/data/multipart.form +0 -0
  42. data/test/data/wabisabi.js +77 -0
  43. data/test/data/wabisabi.js.gz +0 -0
  44. data/test/keight_test.rb +3161 -0
  45. data/test/oktest.rb +1537 -0
  46. metadata +114 -0
@@ -0,0 +1,3161 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ $LOAD_PATH << "lib" unless $LOAD_PATH.include?("lib")
4
+ $LOAD_PATH << "test" unless $LOAD_PATH.include?("test")
5
+
6
+ require 'stringio'
7
+
8
+ require 'oktest'
9
+
10
+ require 'keight'
11
+
12
+
13
+ class BooksAction < K8::Action
14
+ mapping '/', :GET=>:do_index, :POST=>:do_create
15
+ mapping '/new', :GET=>:do_new
16
+ mapping '/{id}', :GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete
17
+ mapping '/{id}/edit', :GET=>:do_edit
18
+ #
19
+ def do_index(); "<index>"; end
20
+ def do_create(); "<create>"; end
21
+ #def do_new(); "<new>"; end
22
+ def do_show(id); "<show:#{id.inspect}(#{id.class})>"; end
23
+ def do_update(id); "<update:#{id.inspect}(#{id.class})>"; end
24
+ def do_delete(id); "<delete:#{id.inspect}(#{id.class})>"; end
25
+ def do_edit(id); "<edit:#{id.inspect}(#{id.class})>"; end
26
+ end
27
+
28
+ class BookCommentsAction < K8::Action
29
+ mapping '/comments', :GET=>:do_comments
30
+ mapping '/comments/{comment_id}', :GET=>:do_comment
31
+ #
32
+ def do_comments(); "<comments>"; end
33
+ def do_comment(comment_id); "<comment:#{comment_id}>"; end
34
+ end
35
+
36
+
37
+ class AdminBooksAction < K8::Action
38
+ mapping '/', :GET=>:do_index, :POST=>:do_create
39
+ mapping '/new', :GET=>:do_new
40
+ mapping '/{id}', :GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete
41
+ mapping '/{id}/edit', :GET=>:do_edit
42
+ #
43
+ def do_index(); end
44
+ def do_create(); end
45
+ def do_show(id); end
46
+ def do_update(id); end
47
+ def do_delete(id); end
48
+ def do_edit(id); end
49
+ end
50
+
51
+
52
+ class TestBaseAction < K8::BaseAction
53
+ def _called
54
+ @_called ||= []
55
+ end
56
+ def before_action(*args)
57
+ self._called << ["before_action", args]
58
+ end
59
+ def after_action(*args)
60
+ self._called << ["after_action", args]
61
+ end
62
+ def handle_content(*args)
63
+ self._called << ["handle_content", args]
64
+ super(*args)
65
+ end
66
+ def handle_exception(*args)
67
+ self._called << ["handle_exception", args]
68
+ super(*args)
69
+ end
70
+ def do_index
71
+ return "<index>"
72
+ end
73
+ def do_create
74
+ 1/0 #=> ZeroDivisionError
75
+ end
76
+ def do_show(id)
77
+ return "<show:#{id}>"
78
+ end
79
+ end
80
+
81
+
82
+ class TestExceptionAction < K8::Action
83
+
84
+ def do_create
85
+ 1/0 #=> ZeroDivisionError
86
+ end
87
+
88
+ end
89
+
90
+
91
+ Oktest.scope do
92
+
93
+ def new_env(meth="GET", path="/", opts={})
94
+ return K8::Mock.new_env(meth, path, opts)
95
+ end
96
+
97
+
98
+ topic K8::Util do
99
+
100
+
101
+ topic '.escape_html()' do
102
+
103
+ spec "[!90jx8] escapes '& < > \" \'' into '&amp; &lt; &gt; &quot; &#39;'." do
104
+ ok {K8::Util.escape_html('& < > " \'')} == '&amp; &lt; &gt; &quot; &#39;'
105
+ end
106
+
107
+ end
108
+
109
+
110
+ topic '.h()' do
111
+
112
+ spec "[!649wt] 'h()' is alias of 'escape_html()'" do
113
+ ok {K8::Util.h('& < > " \'')} == '&amp; &lt; &gt; &quot; &#39;'
114
+ end
115
+
116
+ end
117
+
118
+
119
+ topic '.percent_encode()' do
120
+
121
+ spec "[!a96jo] encodes string into percent encoding format." do
122
+ ok {K8::Util.percent_encode('[xxx]')} == "%5Bxxx%5D"
123
+ end
124
+
125
+ end
126
+
127
+
128
+ topic '.percent_decode()' do
129
+
130
+ spec "[!kl9sk] decodes percent encoded string." do
131
+ ok {K8::Util.percent_decode('%5Bxxx%5D')} == "[xxx]"
132
+ end
133
+
134
+ end
135
+
136
+
137
+ topic '.parse_query_string()' do
138
+
139
+ spec "[!fzt3w] parses query string and returns Hahs object." do
140
+ d = K8::Util.parse_query_string("x=123&y=456&z=")
141
+ ok {d} == {"x"=>"123", "y"=>"456", "z"=>""}
142
+ end
143
+
144
+ spec "[!engr6] returns empty Hash object when query string is empty." do
145
+ d = K8::Util.parse_query_string("")
146
+ ok {d} == {}
147
+ end
148
+
149
+ spec "[!t0w33] regards as array of string when param name ends with '[]'." do
150
+ d = K8::Util.parse_query_string("x[]=123&x[]=456")
151
+ ok {d} == {"x[]"=>["123", "456"]}
152
+ end
153
+
154
+ end
155
+
156
+
157
+ topic '.parse_multipart()' do
158
+
159
+ fixture :multipart_data do
160
+ data_dir = File.join(File.dirname(__FILE__), "data")
161
+ data = File.open("#{data_dir}/multipart.form", 'rb') {|f| f.read() }
162
+ data
163
+ end
164
+
165
+ fixture :boundary do |multipart_data|
166
+ boundary = /\A--(.*)\r\n/.match(multipart_data)[1]
167
+ boundary
168
+ end
169
+
170
+ spec "[!mqrei] parses multipart form data." do
171
+ |multipart_data, boundary|
172
+ begin
173
+ data = multipart_data
174
+ data_dir = File.join(File.dirname(__FILE__), "data")
175
+ stdin = StringIO.new(data)
176
+ params, files = K8::Util.parse_multipart(stdin, boundary, data.length)
177
+ ok {params} == {
178
+ 'text1' => "test1",
179
+ 'text2' => "日本語\r\nあいうえお\r\n".force_encoding('binary'),
180
+ 'file1' => "example1.png",
181
+ 'file2' => "example1.jpg",
182
+ }
183
+ #
184
+ upfile1 = files['file1']
185
+ ok {upfile1}.is_a?(K8::UploadedFile)
186
+ ok {upfile1.filename} == "example1.png"
187
+ ok {upfile1.content_type} == "image/png"
188
+ ok {upfile1.tmp_filepath}.file_exist?
189
+ tmpfile1 = upfile1.tmp_filepath
190
+ ok {File.size(tmpfile1)} == File.size("#{data_dir}/example1.png")
191
+ ok {File.read(tmpfile1)} == File.read("#{data_dir}/example1.png")
192
+ #
193
+ upfile2 = files['file2']
194
+ ok {upfile2}.is_a?(K8::UploadedFile)
195
+ ok {upfile2.filename} == "example1.jpg"
196
+ ok {upfile2.content_type} == "image/jpeg"
197
+ ok {upfile2.tmp_filepath}.file_exist?
198
+ tmpfile2 = upfile2.tmp_filepath
199
+ ok {File.size(tmpfile2)} == File.size("#{data_dir}/example1.jpg")
200
+ ok {File.read(tmpfile2)} == File.read("#{data_dir}/example1.jpg")
201
+ ensure
202
+ files.values.each {|x| x.clean() } if files
203
+ end
204
+ end
205
+
206
+ end
207
+
208
+
209
+ topic '.randstr_b64()' do
210
+
211
+ spec "[!yq0gv] returns random string, encoded with urlsafe base64." do
212
+ arr = (1..1000).map { K8::Util.randstr_b64() }
213
+ ok {arr.sort.uniq.length} == 1000
214
+ arr.each do |s|
215
+ ok {s} =~ /\A[-\w]+\z/
216
+ ok {s.length} == 27
217
+ end
218
+ end
219
+
220
+ end
221
+
222
+
223
+ topic '.guess_content_type()' do
224
+
225
+ spec "[!xw0js] returns content type guessed from filename." do
226
+ ok {K8::Util.guess_content_type("foo.html")} == "text/html"
227
+ ok {K8::Util.guess_content_type("foo.jpg")} == "image/jpeg"
228
+ ok {K8::Util.guess_content_type("foo.json")} == "application/json"
229
+ ok {K8::Util.guess_content_type("foo.xls")} == "application/vnd.ms-excel"
230
+ end
231
+
232
+ spec "[!dku5c] returns 'application/octet-stream' when failed to guess content type." do
233
+ ok {K8::Util.guess_content_type("foo.rbc")} == "application/octet-stream"
234
+ ok {K8::Util.guess_content_type("foo")} == "application/octet-stream"
235
+ end
236
+
237
+ end
238
+
239
+
240
+ topic '#http_utc_time()' do
241
+
242
+ spec "[!5k50b] converts Time object into HTTP date format string." do
243
+ require 'time'
244
+ t = Time.new(2015, 2, 3, 4, 5, 6).utc
245
+ ok {K8::Util.http_utc_time(t)} == t.httpdate
246
+ now = Time.now.utc
247
+ ok {K8::Util.http_utc_time(now)} == now.httpdate
248
+ end
249
+
250
+ spec "[!3z5lf] raises error when argument is not UTC." do
251
+ t = Time.new(2015, 2, 3, 4, 5, 6)
252
+ pr = proc { K8::Util.http_utc_time(t) }
253
+ ok {pr}.raise?(ArgumentError, /\Ahttp_utc_time\(2015-02-03 04:05:06 [-+]\d{4}\): expected UTC time but got local time.\z/)
254
+ end
255
+
256
+ end
257
+
258
+
259
+ end
260
+
261
+
262
+ topic K8::UploadedFile do
263
+
264
+
265
+ topic '#initialize()' do
266
+
267
+ spec "[!ityxj] takes filename and content type." do
268
+ x = K8::UploadedFile.new("hom.html", "text/html")
269
+ ok {x.filename} == "hom.html"
270
+ ok {x.content_type} == "text/html"
271
+ end
272
+
273
+ spec "[!5c8w6] sets temporary filepath with random string." do
274
+ arr = (1..1000).collect { K8::UploadedFile.new("x", "x").tmp_filepath }
275
+ ok {arr.sort.uniq.length} == 1000
276
+ end
277
+
278
+ spec "[!8ezhr] yields with opened temporary file." do
279
+ begin
280
+ s = "homhom"
281
+ x = K8::UploadedFile.new("hom.html", "text/html") {|f| f.write(s) }
282
+ ok {x.tmp_filepath}.file_exist?
283
+ ok {File.open(x.tmp_filepath) {|f| f.read() }} == s
284
+ ensure
285
+ File.unlink(x.tmp_filepath) if File.exist?(x.tmp_filepath)
286
+ end
287
+ end
288
+
289
+ end
290
+
291
+
292
+ topic '#clean()' do
293
+
294
+ spec "[!ft454] removes temporary file if exists." do
295
+ begin
296
+ x = K8::UploadedFile.new("hom.html", "text/html") {|f| f.write("hom") }
297
+ ok {x.tmp_filepath}.file_exist?
298
+ x.clean()
299
+ ok {x.tmp_filepath}.NOT.file_exist?
300
+ ensure
301
+ File.unlink(x.tmp_filepath) if File.exist?(x.tmp_filepath)
302
+ end
303
+ end
304
+
305
+ end
306
+
307
+
308
+ topic '#new_filepath()' do
309
+
310
+ spec "[!zdkts] use $K8_UPLOAD_DIR environment variable as temporary directory." do
311
+ orig = ENV['K8_UPLOAD_DIR']
312
+ ENV['K8_UPLOAD_DIR'] = "/var/tmp/upload"
313
+ begin
314
+ upfile = K8::UploadedFile.new("hom.txt", "text/plain")
315
+ ok {upfile.__send__(:new_filepath)} =~ /\A\/var\/tmp\/upload\/up\./
316
+ ensure
317
+ ENV['K8_UPLOAD_DIR'] = orig
318
+ end
319
+ end
320
+
321
+ end
322
+
323
+ end
324
+
325
+
326
+ topic K8::Request do
327
+
328
+ fixture :req do
329
+ K8::Request.new(new_env("GET", "/123"))
330
+ end
331
+
332
+ fixture :data_dir do
333
+ File.join(File.dirname(__FILE__), "data")
334
+ end
335
+
336
+ fixture :multipart_env do |data_dir|
337
+ input = File.open("#{data_dir}/multipart.form", 'rb') {|f| f.read() }
338
+ boundary = /\A--(.+)\r\n/.match(input)[1]
339
+ cont_type = "multipart/form-data;boundary=#{boundary}"
340
+ env = new_env("POST", "/", input: input, env: {'CONTENT_TYPE'=>cont_type})
341
+ env
342
+ end
343
+
344
+
345
+ topic '#initialize()' do
346
+
347
+ spec "[!yb9k9] sets @env." do
348
+ env = new_env()
349
+ req = K8::Request.new(env)
350
+ ok {req.env}.same?(env)
351
+ end
352
+
353
+ spec "[!yo22o] sets @method as Symbol value." do
354
+ req1 = K8::Request.new(new_env("GET"))
355
+ ok {req1.method} == :GET
356
+ req2 = K8::Request.new(new_env("POST"))
357
+ ok {req2.method} == :POST
358
+ end
359
+
360
+ spec "[!twgmi] sets @path." do
361
+ req1 = K8::Request.new(new_env("GET", "/123"))
362
+ ok {req1.path} == "/123"
363
+ end
364
+
365
+ spec "[!ae8ws] uses SCRIPT_NAME as urlpath when PATH_INFO is not provided." do
366
+ env = new_env("GET", "/123", env: {'SCRIPT_NAME'=>'/index.cgi'})
367
+ env['PATH_INFO'] = ''
368
+ ok {K8::Request.new(env).path} == "/index.cgi"
369
+ env.delete('PATH_INFO')
370
+ ok {K8::Request.new(env).path} == "/index.cgi"
371
+ end
372
+
373
+ end
374
+
375
+
376
+ topic '#header()' do
377
+
378
+ spec "[!1z7wj] returns http header value from environment." do
379
+ env = new_env("GET", "/",
380
+ headers: {'Accept-Encoding'=>'gzip,deflate'},
381
+ env: {'HTTP_ACCEPT_LANGUAGE'=>'en,ja'})
382
+ req = K8::Request.new(env)
383
+ ok {req.header('Accept-Encoding')} == 'gzip,deflate'
384
+ ok {req.header('Accept-Language')} == 'en,ja'
385
+ end
386
+
387
+ end
388
+
389
+
390
+ topic '#method()' do
391
+
392
+ spec "[!tp595] returns :GET, :POST, :PUT, ... when argument is not passed." do
393
+ ok {K8::Request.new(new_env('GET', '/')).method} == :GET
394
+ ok {K8::Request.new(new_env('POST', '/')).method} == :POST
395
+ ok {K8::Request.new(new_env('PUT', '/')).method} == :PUT
396
+ ok {K8::Request.new(new_env('DELETE', '/')).method} == :DELETE
397
+ end
398
+
399
+ spec "[!49f51] returns Method object when argument is passed." do
400
+ req = K8::Request.new(new_env('GET', '/'))
401
+ ok {req.method('env')}.is_a?(Method)
402
+ ok {req.method('env').call()}.same?(req.env)
403
+ end
404
+
405
+ end
406
+
407
+
408
+ topic '#request_method' do
409
+
410
+ spec "[!y8eos] returns env['REQUEST_METHOD'] as string." do
411
+ req = K8::Request.new(new_env(:POST, "/"))
412
+ ok {req.request_method} == "POST"
413
+ end
414
+
415
+ end
416
+
417
+
418
+ topic '#content_type' do
419
+
420
+ spec "[!95g9o] returns env['CONTENT_TYPE']." do
421
+ ctype = "text/html"
422
+ req = K8::Request.new(new_env("GET", "/", env: {'CONTENT_TYPE'=>ctype}))
423
+ ok {req.content_type} == ctype
424
+ req = K8::Request.new(new_env("GET", "/", env: {}))
425
+ ok {req.content_type} == nil
426
+ end
427
+
428
+ end
429
+
430
+
431
+ topic '#content_length' do
432
+
433
+ spec "[!0wbek] returns env['CONTENT_LENGHT'] as integer." do
434
+ req = K8::Request.new(new_env("GET", "/", env: {'CONTENT_LENGTH'=>'0'}))
435
+ ok {req.content_length} == 0
436
+ req.env.delete('CONTENT_LENGTH')
437
+ ok {req.content_length} == nil
438
+ end
439
+
440
+ end
441
+
442
+
443
+ topic '#xhr?' do
444
+
445
+ spec "[!hsgkg] returns true when 'X-Requested-With' header is 'XMLHttpRequest'." do
446
+ env = new_env("GET", "/", headers: {'X-Requested-With'=>'XMLHttpRequest'})
447
+ ok {K8::Request.new(env).xhr?} == true
448
+ env = new_env("GET", "/", headers: {})
449
+ ok {K8::Request.new(env).xhr?} == false
450
+ end
451
+
452
+ end
453
+
454
+
455
+ topic '#client_ip_addr' do
456
+
457
+ spec "[!e1uvg] returns 'X-Real-IP' header value if provided." do
458
+ env = new_env("GET", "/",
459
+ headers: {'X-Real-IP'=>'192.168.1.23'},
460
+ env: {'REMOTE_ADDR'=>'192.168.0.1'})
461
+ ok {K8::Request.new(env).client_ip_addr} == '192.168.1.23'
462
+ end
463
+
464
+ spec "[!qdlyl] returns first item of 'X-Forwarded-For' header if provided." do
465
+ env = new_env("GET", "/",
466
+ headers: {'X-Forwarded-For'=>'192.168.1.1, 192.168.1.2, 192.168.1.3'},
467
+ env: {'REMOTE_ADDR'=>'192.168.0.1'})
468
+ ok {K8::Request.new(env).client_ip_addr} == '192.168.1.1'
469
+ end
470
+
471
+ spec "[!8nzjh] returns 'REMOTE_ADDR' if neighter 'X-Real-IP' nor 'X-Forwarded-For' provided." do
472
+ env = new_env("GET", "/",
473
+ env: {'REMOTE_ADDR'=>'192.168.0.1'})
474
+ ok {K8::Request.new(env).client_ip_addr} == '192.168.0.1'
475
+ end
476
+
477
+ end
478
+
479
+
480
+ topic '#scheme' do
481
+
482
+ spec "[!jytwy] returns 'https' when env['HTTPS'] is 'on'." do
483
+ env = new_env("GET", "/", env: {'HTTPS'=>'on'})
484
+ ok {K8::Request.new(env).scheme} == 'https'
485
+ end
486
+
487
+ spec "[!zg8r2] returns env['rack.url_scheme'] ('http' or 'https')." do
488
+ env = new_env("GET", "/", env: {'HTTPS'=>'off'})
489
+ env['rack.url_scheme'] = 'http'
490
+ ok {K8::Request.new(env).scheme} == 'http'
491
+ env['rack.url_scheme'] = 'https'
492
+ ok {K8::Request.new(env).scheme} == 'https'
493
+ end
494
+
495
+ end
496
+
497
+
498
+ topic '#params_query' do
499
+
500
+ spec "[!6ezqw] parses QUERY_STRING and returns it as Hash object." do
501
+ qstr = "x=1&y=2"
502
+ req = K8::Request.new(new_env("GET", "/", env: {'QUERY_STRING'=>qstr}))
503
+ ok {req.params_query()} == {'x'=>'1', 'y'=>'2'}
504
+ end
505
+
506
+ spec "[!o0ws7] unquotes both keys and values." do
507
+ qstr = "arr%5Bxxx%5D=%3C%3E+%26%3B"
508
+ req = K8::Request.new(new_env("GET", "/", env: {'QUERY_STRING'=>qstr}))
509
+ ok {req.params_query()} == {'arr[xxx]'=>'<> &;'}
510
+ end
511
+
512
+ end
513
+
514
+
515
+ topic '#params_form' do
516
+
517
+ spec "[!q88w9] raises error when content length is missing." do
518
+ env = new_env("POST", "/", form: "x=1")
519
+ env['CONTENT_LENGTH'] = nil
520
+ req = K8::Request.new(env)
521
+ pr = proc { req.params_form }
522
+ ok {pr}.raise?(K8::HttpException, 'Content-Length header expected.')
523
+ end
524
+
525
+ spec "[!gi4qq] raises error when content length is invalid." do
526
+ env = new_env("POST", "/", form: "x=1")
527
+ env['CONTENT_LENGTH'] = "abc"
528
+ req = K8::Request.new(env)
529
+ pr = proc { req.params_form }
530
+ ok {pr}.raise?(K8::HttpException, 'Content-Length should be an integer.')
531
+ end
532
+
533
+ spec "[!59ad2] parses form parameters and returns it as Hash object when form requested." do
534
+ form = "x=1&y=2&arr%5Bxxx%5D=%3C%3E+%26%3B"
535
+ req = K8::Request.new(new_env("POST", "/", form: form))
536
+ ok {req.params_form} == {'x'=>'1', 'y'=>'2', 'arr[xxx]'=>'<> &;'}
537
+ end
538
+
539
+ spec "[!puxlr] raises error when content length is too long (> 10MB)." do
540
+ env = new_env("POST", "/", form: "x=1")
541
+ env['CONTENT_LENGTH'] = (10*1024*1024 + 1).to_s
542
+ req = K8::Request.new(env)
543
+ pr = proc { req.params_form }
544
+ ok {pr}.raise?(K8::HttpException, 'Content-Length is too long.')
545
+ end
546
+
547
+ end
548
+
549
+
550
+ topic '#params_multipart' do
551
+
552
+ spec "[!y1jng] parses multipart when multipart form requested." do
553
+ |multipart_env, data_dir|
554
+ env = multipart_env
555
+ req = K8::Request.new(env)
556
+ form, files = req.params_multipart
557
+ ok {form} == {
558
+ "text1" => "test1",
559
+ "text2" => "日本語\r\nあいうえお\r\n".force_encoding('binary'),
560
+ "file1" => "example1.png",
561
+ "file2" => "example1.jpg",
562
+ }
563
+ ok {files}.is_a?(Hash)
564
+ ok {files.keys.sort} == ["file1", "file2"]
565
+ #
566
+ ok {files['file1']}.is_a?(K8::UploadedFile)
567
+ ok {files['file1'].filename} == "example1.png"
568
+ ok {files['file1'].content_type} == "image/png"
569
+ ok {files['file1'].tmp_filepath}.file_exist?
570
+ expected = File.read("#{data_dir}/example1.png", encoding: 'binary')
571
+ actual = File.read(files['file1'].tmp_filepath, encoding: 'binary')
572
+ ok {actual} == expected
573
+ #
574
+ ok {files['file2']}.is_a?(K8::UploadedFile)
575
+ ok {files['file2'].filename} == "example1.jpg"
576
+ ok {files['file2'].content_type} == "image/jpeg"
577
+ ok {files['file2'].tmp_filepath}.file_exist?
578
+ expected = File.read("#{data_dir}/example1.jpg", encoding: 'binary')
579
+ actual = File.read(files['file2'].tmp_filepath, encoding: 'binary')
580
+ ok {actual} == expected
581
+
582
+ end
583
+
584
+ spec "[!mtx6t] raises error when content length of multipart is too long (> 100MB)." do
585
+ |multipart_env|
586
+ env = multipart_env
587
+ env['CONTENT_LENGTH'] = (100*1024*1024 + 1).to_s
588
+ req = K8::Request.new(env)
589
+ pr = proc { req.params_multipart }
590
+ ok {pr}.raise?(K8::HttpException, 'Content-Length of multipart is too long.')
591
+ end
592
+
593
+ end
594
+
595
+
596
+ topic '#params_json' do
597
+
598
+ spec "[!ugik5] parses json data and returns it as hash object when json data is sent." do
599
+ data = '{"x":1,"y":2,"arr":["a","b","c"]}'
600
+ req = K8::Request.new(new_env("POST", "/", json: data))
601
+ ok {req.params_json} == {"x"=>1, "y"=>2, "arr"=>["a", "b", "c"]}
602
+ end
603
+
604
+ end
605
+
606
+
607
+ topic '#params' do
608
+
609
+ spec "[!erlc7] parses QUERY_STRING when request method is GET or HEAD." do
610
+ qstr = "a=8&b=9"
611
+ form = "x=1&y=2"
612
+ req = K8::Request.new(new_env('GET', '/', query: qstr, form: form))
613
+ ok {req.params} == {"a"=>"8", "b"=>"9"}
614
+ end
615
+
616
+ spec "[!cr0zj] parses JSON when content type is 'application/json'." do
617
+ qstr = "a=8&b=9"
618
+ json = '{"n":123}'
619
+ req = K8::Request.new(new_env('POST', '/', query: qstr, json: json))
620
+ ok {req.params} == {"n"=>123}
621
+ end
622
+
623
+ spec "[!j2lno] parses form parameters when content type is 'application/x-www-form-urlencoded'." do
624
+ qstr = "a=8&b=9"
625
+ form = "x=1&y=2"
626
+ req = K8::Request.new(new_env('POST', '/', query: qstr, form: form))
627
+ ok {req.params} == {"x"=>"1", "y"=>"2"}
628
+ end
629
+
630
+ spec "[!4rmn9] parses multipart when content type is 'multipart/form-data'."
631
+
632
+ end
633
+
634
+
635
+ topic '#cookies' do
636
+
637
+ spec "[!c9pwr] parses cookie data and returns it as hash object." do
638
+ req = K8::Request.new(new_env('POST', '/', cookie: "aaa=homhom; bbb=madmad"))
639
+ ok {req.cookies} == {"aaa"=>"homhom", "bbb"=>"madmad"}
640
+ end
641
+
642
+ end
643
+
644
+
645
+ topic '#clear()' do
646
+
647
+ spec "[!0jdal] removes uploaded files." do
648
+ |multipart_env|
649
+ req = K8::Request.new(multipart_env)
650
+ form, files = req.params_multipart
651
+ ok {files.empty?} == false
652
+ tmpfile1 = files['file1'].tmp_filepath
653
+ tmpfile2 = files['file2'].tmp_filepath
654
+ ok {tmpfile1}.file_exist?
655
+ ok {tmpfile2}.file_exist?
656
+ #
657
+ req.clear()
658
+ ok {tmpfile1}.NOT.file_exist?
659
+ ok {tmpfile2}.NOT.file_exist?
660
+ end
661
+
662
+ end
663
+
664
+
665
+ end
666
+
667
+
668
+ topic K8::Response do
669
+ end
670
+
671
+
672
+ topic 'K8::REQUEST_CLASS=' do
673
+
674
+ spec "[!7uqb4] changes default request class." do
675
+ original = K8::REQUEST_CLASS
676
+ begin
677
+ K8.REQUEST_CLASS = Array
678
+ ok {K8::REQUEST_CLASS} == Array
679
+ ensure
680
+ K8.REQUEST_CLASS = original
681
+ end
682
+ end
683
+
684
+ end
685
+
686
+
687
+ topic 'K8::RESPONSE_CLASS=' do
688
+
689
+ spec "[!c1bd0] changes default response class." do
690
+ original = K8::RESPONSE_CLASS
691
+ begin
692
+ K8.RESPONSE_CLASS = Hash
693
+ ok {K8::RESPONSE_CLASS} == Hash
694
+ ensure
695
+ K8.RESPONSE_CLASS = original
696
+ end
697
+ end
698
+
699
+ end
700
+
701
+
702
+ topic K8::BaseAction do
703
+
704
+ fixture :action do
705
+ env = new_env("GET", "/books")
706
+ TestBaseAction.new(K8::Request.new(env), K8::Response.new())
707
+ end
708
+
709
+
710
+ topic '#initialize()' do
711
+
712
+ spec "[!uotpb] accepts request and response objects." do
713
+ req = K8::Request.new(new_env("GET", "/books"))
714
+ resp = K8::Response.new()
715
+ action = K8::BaseAction.new(req, resp)
716
+ ok {action.instance_variable_get('@req')}.same?(req)
717
+ ok {action.instance_variable_get('@resp')}.same?(resp)
718
+ end
719
+
720
+ spec "[!7sfyf] sets session object." do
721
+ d = {'a'=>1}
722
+ req = K8::Request.new(new_env("GET", "/books", env: {'rack.session'=>d}))
723
+ resp = K8::Response.new()
724
+ action = K8::BaseAction.new(req, resp)
725
+ ok {action.instance_variable_get('@sess')}.same?(d)
726
+ ok {action.sess}.same?(d)
727
+ end
728
+
729
+ end
730
+
731
+
732
+ topic '#handle_action()' do
733
+
734
+ spec "[!ddgx3] invokes action method with urlpath params." do
735
+ |action|
736
+ ok {action.handle_action(:do_show, [123])} == "<show:123>"
737
+ end
738
+
739
+ spec "[!aqa4e] returns content." do
740
+ |action|
741
+ ok {action.handle_action(:do_index, [])} == "<index>"
742
+ ok {action._called[1]} == ["handle_content", ["<index>"]]
743
+ end
744
+
745
+ spec "[!5jnx6] calls '#before_action()' before handling request." do
746
+ |action|
747
+ action.handle_action(:do_index, [])
748
+ ok {action._called[0]} == ["before_action", []]
749
+ end
750
+
751
+ spec "[!67awf] calls '#after_action()' after handling request." do
752
+ |action|
753
+ action.handle_action(:do_index, [])
754
+ ok {action._called[-1]} == ["after_action", [nil]]
755
+ end
756
+
757
+ spec "[!alpka] calls '#after_action()' even when error raised." do
758
+ |action|
759
+ pr = proc { action.handle_action(:do_create, []) }
760
+ ok {pr}.raise?(ZeroDivisionError)
761
+ ok {action._called[-1]} == ["after_action", [pr.exception]]
762
+ end
763
+
764
+ end
765
+
766
+
767
+ topic '.mapping()' do
768
+
769
+ spec "[!o148k] maps urlpath pattern and request methods." do
770
+ cls = Class.new(K8::BaseAction) do
771
+ mapping '', :GET=>:do_index, :POST=>:do_create
772
+ mapping '/{code}', :GET=>:do_show, :PUT=>:do_update
773
+ end
774
+ args_list = []
775
+ cls._action_method_mapping.each do |*args|
776
+ args_list << args
777
+ end
778
+ ok {args_list} == [
779
+ ["", {:GET=>:do_index, :POST=>:do_create}],
780
+ ["/{code}", {:GET=>:do_show, :PUT=>:do_update}],
781
+ ]
782
+ end
783
+
784
+ end
785
+
786
+
787
+ end
788
+
789
+
790
+ topic K8::Action do
791
+
792
+ fixture :action_obj do
793
+ env = new_env("GET", "/", env: {'rack.session'=>{}})
794
+ BooksAction.new(K8::Request.new(env), K8::Response.new())
795
+ end
796
+
797
+
798
+ topic '#request' do
799
+
800
+ spec "[!siucz] request object is accessable with 'request' method as well as 'req'." do
801
+ |action_obj|
802
+ ok {action_obj.request}.same?(action_obj.req)
803
+ end
804
+
805
+ end
806
+
807
+
808
+ topic '#response' do
809
+
810
+ spec "[!qnzp6] response object is accessable with 'response' method as well as 'resp'." do
811
+ |action_obj|
812
+ ok {action_obj.response}.same?(action_obj.resp)
813
+ end
814
+
815
+ end
816
+
817
+
818
+ topic '#session' do
819
+
820
+ spec "[!bd3y4] session object is accessable with 'session' method as well as 'sess'." do
821
+ |action_obj|
822
+ ok {action_obj.session}.same?(action_obj.sess)
823
+ ok {action_obj.session} != nil
824
+ end
825
+
826
+ end
827
+
828
+
829
+ topic '#before_action()' do
830
+ end
831
+
832
+
833
+ topic '#after_action()' do
834
+
835
+ spec "[!qsz2z] raises ContentTypeRequiredError when content type is not set." do
836
+ |action_obj|
837
+ action_obj.instance_exec(self) do |_|
838
+ _.ok {@resp.headers.key?('Content-Type')} == false
839
+ pr = proc { after_action(nil) }
840
+ _.ok {pr}.raise?(K8::ContentTypeRequiredError)
841
+ end
842
+ end
843
+
844
+ end
845
+
846
+
847
+ topic '#invoke_action()' do
848
+
849
+ spec "[!d5v0l] handles exception when handler method defined." do
850
+ env = new_env("POST", "/", env: {'rack.session'=>{}})
851
+ action_obj = TestExceptionAction.new(K8::Request.new(env), K8::Response.new())
852
+ result = nil
853
+ pr = proc { result = action_obj.handle_action(:do_create, []) }
854
+ ok {pr}.raise?(ZeroDivisionError)
855
+ #
856
+ action_obj.instance_exec(self) do |_|
857
+ def on_ZeroDivisionError(ex)
858
+ @_called = ex
859
+ "<h1>Yes</h1>"
860
+ end
861
+ end
862
+ ok {action_obj}.respond_to?('on_ZeroDivisionError')
863
+ ok {pr}.NOT.raise?(ZeroDivisionError)
864
+ ok {action_obj.instance_variable_get('@_called')} != nil
865
+ ok {action_obj.instance_variable_get('@_called')}.is_a?(ZeroDivisionError)
866
+ ok {result} == ["<h1>Yes</h1>"]
867
+ end
868
+
869
+ end
870
+
871
+
872
+ topic '#handle_content()' do
873
+
874
+ case_when "[!jhnzu] when content is nil..." do
875
+
876
+ spec "[!sfwfz] returns ['']." do
877
+ |action_obj|
878
+ action_obj.instance_exec(self) do |_|
879
+ _.ok {handle_content(nil)} == ['']
880
+ end
881
+ end
882
+
883
+ end
884
+
885
+ case_when "[!lkxua] when content is a hash object..." do
886
+
887
+ spec "[!9aaxl] converts hash object into JSON string." do
888
+ |action_obj|
889
+ action_obj.instance_exec(self) do |_|
890
+ _.ok {handle_content({"a"=>nil})} == ['{"a":null}']
891
+ end
892
+ end
893
+
894
+ spec "[!c7nj7] sets content length." do
895
+ |action_obj|
896
+ action_obj.instance_exec(self) do |_|
897
+ handle_content({"a"=>nil})
898
+ _.ok {'{"a":null}'.bytesize} == 10
899
+ _.ok {@resp.headers['Content-Length']} == "10"
900
+ end
901
+ end
902
+
903
+ spec "[!j0c1d] sets content type as 'application/json' when not set." do
904
+ |action_obj|
905
+ action_obj.instance_exec(self) do |_|
906
+ handle_content({"a"=>nil})
907
+ _.ok {@resp.headers['Content-Type']} == "application/json"
908
+ end
909
+ end
910
+
911
+ spec "[!gw05f] returns array of JSON string." do
912
+ |action_obj|
913
+ action_obj.instance_exec(self) do |_|
914
+ _.ok {handle_content({"a"=>nil})} == ['{"a":null}']
915
+ end
916
+ end
917
+
918
+ end
919
+
920
+ case_when "[!p6p99] when content is a string..." do
921
+
922
+ spec "[!1ejgh] sets content length." do
923
+ |action_obj|
924
+ action_obj.instance_exec(self) do |_|
925
+ handle_content("<b>")
926
+ _.ok {@resp.headers['Content-Length']} == "3"
927
+ end
928
+ end
929
+
930
+ spec "[!uslm5] sets content type according to content when not set." do
931
+ |action_obj|
932
+ action_obj.instance_exec(self) do |_|
933
+ handle_content("<html>")
934
+ _.ok {@resp.headers['Content-Type']} == "text/html; charset=utf-8"
935
+ #
936
+ @resp.headers['Content-Type'] = nil
937
+ handle_content('{"a":1}')
938
+ _.ok {@resp.headers['Content-Type']} == "application/json"
939
+ end
940
+ end
941
+
942
+ spec "[!5q1u5] raises error when failed to detect content type." do
943
+ |action_obj|
944
+ action_obj.instance_exec(self) do |_|
945
+ pr = proc { handle_content("html") }
946
+ _.ok {pr}.raise?(K8::ContentTypeRequiredError, "Content-Type response header required.")
947
+ end
948
+ end
949
+
950
+ spec "[!79v6x] returns array of string." do
951
+ |action_obj|
952
+ action_obj.instance_exec(self) do |_|
953
+ _.ok {handle_content("<html>")} == ["<html>"]
954
+ end
955
+ end
956
+
957
+ end
958
+
959
+ case_when "[!s7eix] when content is an Enumerable object..." do
960
+
961
+ spec "[!md2go] just returns content." do
962
+ |action_obj|
963
+ action_obj.instance_exec(self) do |_|
964
+ arr = ["A", "B", "C"]
965
+ _.ok {handle_content(arr)}.same?(arr)
966
+ end
967
+ end
968
+
969
+ spec "[!ab3vr] neither content length nor content type are not set." do
970
+ |action_obj|
971
+ action_obj.instance_exec(self) do |_|
972
+ handle_content(["A", "B", "C"])
973
+ _.ok {@resp.headers['Content-Length']} == nil
974
+ _.ok {@resp.headers['Content-Type']} == nil
975
+ end
976
+ end
977
+
978
+ end
979
+
980
+ case_when "[!apwh4] else..." do
981
+
982
+ spec "[!wmgnr] raises K8::UnknownContentError." do
983
+ |action_obj|
984
+ action_obj.instance_exec(self) do |_|
985
+ pr1 = proc { handle_content(123) }
986
+ _.ok {pr1}.raise?(K8::UnknownContentError)
987
+ pr2 = proc { handle_content(true) }
988
+ _.ok {pr2}.raise?(K8::UnknownContentError)
989
+ end
990
+ end
991
+
992
+ end
993
+
994
+ end
995
+
996
+
997
+ topic '#detect_content_type()' do
998
+
999
+ spec "[!onjro] returns 'text/html; charset=utf-8' when text starts with '<'." do
1000
+ |action_obj|
1001
+ action_obj.instance_exec(self) do |_|
1002
+ ctype = 'text/html; charset=utf-8'
1003
+ _.ok {detect_content_type("<p>Hello</p>")} == ctype
1004
+ _.ok {detect_content_type("\n\n<p>Hello</p>")} == ctype
1005
+ end
1006
+ end
1007
+
1008
+ spec "[!qiugc] returns 'application/json' when text starts with '{'." do
1009
+ |action_obj|
1010
+ action_obj.instance_exec(self) do |_|
1011
+ ctype = 'application/json'
1012
+ _.ok {detect_content_type("{\"a\":1}")} == ctype
1013
+ _.ok {detect_content_type("\n\n{\"a\":1}")} == ctype
1014
+ end
1015
+ end
1016
+
1017
+ spec "[!zamnv] returns nil when text starts with neight '<' nor '{'." do
1018
+ |action_obj|
1019
+ action_obj.instance_exec(self) do |_|
1020
+ _.ok {detect_content_type("hoomhom")} == nil
1021
+ _.ok {detect_content_type("\n\nhomhom")} == nil
1022
+ end
1023
+ end
1024
+
1025
+ end
1026
+
1027
+
1028
+ topic '#set_flash_message()' do
1029
+
1030
+ spec "[!9f0iv] sets flash message into session." do
1031
+ |action_obj|
1032
+ action_obj.instance_exec(self) do |_|
1033
+ @sess = {}
1034
+ action_obj.set_flash_message("homhom")
1035
+ _.ok {@sess} == {"_flash"=>"homhom"}
1036
+ end
1037
+ end
1038
+
1039
+ end
1040
+
1041
+
1042
+ topic '#get_flash_message()' do
1043
+
1044
+ spec "[!5minm] returns flash message stored in session." do
1045
+ |action_obj|
1046
+ action_obj.instance_exec(self) do |_|
1047
+ @sess = {}
1048
+ action_obj.set_flash_message("homhom")
1049
+ _.ok {action_obj.get_flash_message()} == "homhom"
1050
+ end
1051
+ end
1052
+
1053
+ spec "[!056bp] deletes flash message from sesson." do
1054
+ |action_obj|
1055
+ action_obj.instance_exec(self) do |_|
1056
+ @sess = {}
1057
+ action_obj.set_flash_message("homhom")
1058
+ _.ok {@sess.empty?} == false
1059
+ action_obj.get_flash_message()
1060
+ _.ok {@sess.empty?} == true
1061
+ end
1062
+ end
1063
+
1064
+ end
1065
+
1066
+
1067
+ topic '#redirect_to()' do
1068
+
1069
+ spec "[!ev9nu] sets response status code as 302." do
1070
+ |action_obj|
1071
+ action_obj.instance_exec(self) do |_|
1072
+ redirect_to '/top'
1073
+ _.ok {@resp.status} == 302
1074
+ redirect_to '/top', 301
1075
+ _.ok {@resp.status} == 301
1076
+ end
1077
+ end
1078
+
1079
+ spec "[!spfge] sets Location response header." do
1080
+ |action_obj|
1081
+ action_obj.instance_exec(self) do |_|
1082
+ redirect_to '/top'
1083
+ _.ok {@resp.headers['Location']} == '/top'
1084
+ end
1085
+ end
1086
+
1087
+ spec "[!k3gvm] returns html anchor tag." do
1088
+ |action_obj|
1089
+ action_obj.instance_exec(self) do |_|
1090
+ ret = redirect_to '/top?x=1&y=2'
1091
+ _.ok {ret} == '<a href="/top?x=1&amp;y=2">/top?x=1&amp;y=2</a>'
1092
+ end
1093
+ end
1094
+
1095
+ spec "[!xkrfk] sets flash message if provided." do
1096
+ |action_obj|
1097
+ action_obj.instance_exec(self) do |_|
1098
+ redirect_to '/top', flash: "created!"
1099
+ _.ok {get_flash_message()} == "created!"
1100
+ end
1101
+ end
1102
+
1103
+ end
1104
+
1105
+
1106
+ topic '#validation_failed()' do
1107
+
1108
+ spec "[!texnd] sets response status code as 422." do
1109
+ |action_obj|
1110
+ action_obj.instance_exec(self) do |_|
1111
+ validation_failed()
1112
+ _.ok {@resp.status} == 422
1113
+ end
1114
+ end
1115
+
1116
+ end
1117
+
1118
+
1119
+ topic '#csrf_protection_required?' do
1120
+
1121
+ fixture :action_obj do
1122
+ env = new_env('GET', '/')
1123
+ action = K8::Action.new(K8::Request.new(env), K8::Response.new)
1124
+ end
1125
+
1126
+ spec "[!8chgu] returns false when requested with 'XMLHttpRequest'." do
1127
+ headers = {'X-Requested-With'=>'XMLHttpRequest'}
1128
+ env = new_env('GET', '/', headers: headers)
1129
+ action = K8::Action.new(K8::Request.new(env), K8::Response.new)
1130
+ action.instance_exec(self) do |_|
1131
+ _.ok {csrf_protection_required?} == false
1132
+ end
1133
+ end
1134
+
1135
+ spec "[!vwrqv] returns true when request method is one of POST, PUT, or DELETE." do
1136
+ ['POST', 'PUT', 'DELETE'].each do |meth|
1137
+ env = new_env(meth, '/')
1138
+ action = K8::Action.new(K8::Request.new(env), K8::Response.new)
1139
+ action.instance_exec(self) do |_|
1140
+ _.ok {csrf_protection_required?} == true
1141
+ end
1142
+ end
1143
+ end
1144
+
1145
+ spec "[!jfhla] returns true when request method is GET or HEAD." do
1146
+ ['GET', 'HEAD'].each do |meth|
1147
+ env = new_env(meth, '/')
1148
+ action = K8::Action.new(K8::Request.new(env), K8::Response.new)
1149
+ action.instance_exec(self) do |_|
1150
+ _.ok {csrf_protection_required?} == false
1151
+ end
1152
+ end
1153
+ end
1154
+
1155
+ end
1156
+
1157
+
1158
+ topic '#csrf_protection()' do
1159
+
1160
+ spec "[!h5tzb] raises nothing when csrf token matched." do
1161
+ headers = {'Cookie'=>"_csrf=abc123"}
1162
+ form = {"_csrf"=>"abc123"}
1163
+ env = new_env('POST', '/', form: form, headers: headers)
1164
+ action = K8::Action.new(K8::Request.new(env), K8::Response.new)
1165
+ action.instance_exec(self) do |_|
1166
+ pr = proc { csrf_protection() }
1167
+ _.ok {pr}.NOT.raise?
1168
+ end
1169
+ end
1170
+
1171
+ spec "[!h0e0q] raises HTTP 400 when csrf token mismatched." do
1172
+ headers = {'Cookie'=>"_csrf=abc123"}
1173
+ form = {"_csrf"=>"abc999"}
1174
+ env = new_env('POST', '/', form: form, headers: headers)
1175
+ action = K8::Action.new(K8::Request.new(env), K8::Response.new)
1176
+ action.instance_exec(self) do |_|
1177
+ pr = proc { csrf_protection() }
1178
+ _.ok {pr}.raise?(K8::HttpException, "invalid csrf token")
1179
+ end
1180
+ end
1181
+
1182
+ end
1183
+
1184
+
1185
+ topic '#csrf_get_token()' do
1186
+
1187
+ spec "[!mr6md] returns csrf cookie value." do
1188
+ |action_obj|
1189
+ action_obj.instance_exec(self) do |_|
1190
+ @req.env['HTTP_COOKIE'] = "_csrf=abc123"
1191
+ _.ok {csrf_get_token()} == "abc123"
1192
+ end
1193
+ end
1194
+
1195
+ end
1196
+
1197
+
1198
+ topic '#csrf_set_token()' do
1199
+
1200
+ spec "[!8hm2o] sets csrf cookie and returns token." do
1201
+ |action_obj|
1202
+ action_obj.instance_exec(self) do |_|
1203
+ ret = csrf_set_token("abcdef123456")
1204
+ _.ok {@resp.headers['Set-Cookie']} == "_csrf=abcdef123456"
1205
+ _.ok {ret} == "abcdef123456"
1206
+ end
1207
+ end
1208
+
1209
+ end
1210
+
1211
+
1212
+ topic '#csrf_get_param()' do
1213
+
1214
+ spec "[!pal33] returns csrf token in request parameter." do
1215
+ env = new_env("POST", "/", form: {"_csrf"=>"foobar999"})
1216
+ action_obj = K8::Action.new(K8::Request.new(env), K8::Response.new)
1217
+ action_obj.instance_exec(self) do |_|
1218
+ _.ok {csrf_get_param()} == "foobar999"
1219
+ end
1220
+ end
1221
+
1222
+ end
1223
+
1224
+
1225
+ topic '#csrf_new_token()' do
1226
+
1227
+ spec "[!zl6cl] returns new random token." do
1228
+ |action_obj|
1229
+ tokens = []
1230
+ n = 1000
1231
+ action_obj.instance_exec(self) do |_|
1232
+ n.times { tokens << csrf_new_token() }
1233
+ end
1234
+ ok {tokens.sort.uniq.length} == n
1235
+ end
1236
+
1237
+ spec "[!sfgfx] uses SHA1 + urlsafe BASE64." do
1238
+ |action_obj|
1239
+ action_obj.instance_exec(self) do |_|
1240
+ token = (1..5).each.map { csrf_new_token() }.find {|x| x =~ /[^a-fA-F0-9]/ }
1241
+ _.ok {token} != nil
1242
+ _.ok {token} =~ /\A[-_a-zA-Z0-9]+\z/ # uses urlsafe BASE64
1243
+ _.ok {token.length} == 27 # == SHA1.length - "=".length
1244
+ end
1245
+ end
1246
+
1247
+ end
1248
+
1249
+
1250
+ topic '#csrf_token()' do
1251
+
1252
+ spec "[!7gibo] returns current csrf token." do
1253
+ |action_obj|
1254
+ action_obj.instance_exec(self) do |_|
1255
+ token = csrf_token()
1256
+ _.ok {token} =~ /\A[-_a-zA-Z0-9]{27}\z/
1257
+ _.ok {csrf_token()} == token
1258
+ _.ok {csrf_token()} == token
1259
+ end
1260
+ end
1261
+
1262
+ spec "[!6vtqd] creates new csrf token and set it to cookie when csrf token is blank." do
1263
+ |action_obj|
1264
+ action_obj.instance_exec(self) do |_|
1265
+ _.ok {@resp.headers['Set-Cookie']} == nil
1266
+ token = csrf_token()
1267
+ _.ok {@resp.headers['Set-Cookie']} == "_csrf=#{token}"
1268
+ end
1269
+ end
1270
+
1271
+ end
1272
+
1273
+
1274
+ topic '#send_file()' do
1275
+
1276
+ fixture :data_dir do
1277
+ File.join(File.dirname(__FILE__), 'data')
1278
+ end
1279
+
1280
+ fixture :pngfile do |data_dir|
1281
+ File.join(data_dir, 'example1.png')
1282
+ end
1283
+
1284
+ fixture :jpgfile do |data_dir|
1285
+ File.join(data_dir, 'example1.jpg')
1286
+ end
1287
+
1288
+ fixture :jsfile do |data_dir|
1289
+ File.join(data_dir, 'wabisabi.js')
1290
+ end
1291
+
1292
+ spec "[!37i9c] returns opened file." do
1293
+ |action_obj, jpgfile|
1294
+ action_obj.instance_exec(self) do |_|
1295
+ file = send_file(jpgfile)
1296
+ _.ok {file}.is_a?(File)
1297
+ _.ok {file.closed?} == false
1298
+ _.ok {file.path} == jpgfile
1299
+ end
1300
+ end
1301
+
1302
+ spec "[!v7r59] returns nil with status code 304 when not modified." do
1303
+ |action_obj, pngfile|
1304
+ mtime_utc_str = K8::Util.http_utc_time(File.mtime(pngfile).utc)
1305
+ action_obj.instance_exec(self) do |_|
1306
+ @req.env['HTTP_IF_MODIFIED_SINCE'] = mtime_utc_str
1307
+ ret = send_file(pngfile)
1308
+ _.ok {ret} == nil
1309
+ _.ok {@resp.status} == 304
1310
+ end
1311
+ end
1312
+
1313
+ case_when "[!woho6] when gzipped file exists..." do
1314
+
1315
+ spec "[!9dmrf] returns gzipped file object when 'Accept-Encoding: gzip' exists." do
1316
+ |action_obj, jsfile|
1317
+ action_obj.instance_exec(self) do |_|
1318
+ file = send_file(jsfile)
1319
+ _.ok {file}.is_a?(File)
1320
+ _.ok {file.path} == jsfile # not gzipped
1321
+ #
1322
+ @req.env['HTTP_ACCEPT_ENCODING'] = 'gzip,deflate'
1323
+ file = send_file(jsfile)
1324
+ _.ok {file}.is_a?(File)
1325
+ _.ok {file.path} == jsfile + ".gz"
1326
+ end
1327
+ end
1328
+
1329
+ spec "[!m51dk] adds 'Content-Encoding: gzip' when 'Accept-Encoding: gzip' exists." do
1330
+ |action_obj, jsfile|
1331
+ action_obj.instance_exec(self) do |_|
1332
+ @resp.headers.clear()
1333
+ send_file(jsfile)
1334
+ _.ok {@resp.headers['Content-Encoding']} == nil
1335
+ _.ok {@resp.headers['Content-Type']} == 'application/javascript'
1336
+ _.ok {@resp.status} == 200
1337
+ #
1338
+ @resp.headers.clear()
1339
+ @req.env['HTTP_ACCEPT_ENCODING'] = 'gzip,deflate'
1340
+ send_file(jsfile)
1341
+ _.ok {@resp.headers['Content-Encoding']} == 'gzip'
1342
+ _.ok {@resp.headers['Content-Type']} == 'application/javascript'
1343
+ _.ok {@resp.status} == 200
1344
+ end
1345
+ end
1346
+
1347
+ end
1348
+
1349
+
1350
+ spec "[!e8l5o] sets Content-Type with guessing it from filename." do
1351
+ |action_obj, pngfile, jpgfile|
1352
+ action_obj.instance_exec(self) do |_|
1353
+ send_file(pngfile)
1354
+ _.ok {@resp.headers['Content-Type']} == "image/png"
1355
+ #
1356
+ send_file(jpgfile)
1357
+ _.ok {@resp.headers['Content-Type']} == "image/png" # not changed
1358
+ #
1359
+ @resp.headers['Content-Type'] = nil
1360
+ send_file(jpgfile)
1361
+ _.ok {@resp.headers['Content-Type']} == "image/jpeg" # changed!
1362
+ end
1363
+ end
1364
+
1365
+ spec "[!qhx0l] sets Content-Length with file size." do
1366
+ |action_obj, pngfile, jpgfile|
1367
+ action_obj.instance_exec(self) do |_|
1368
+ send_file(pngfile)
1369
+ _.ok {@resp.headers['Content-Length']} == File.size(pngfile).to_s
1370
+ send_file(jpgfile)
1371
+ _.ok {@resp.headers['Content-Length']} == File.size(jpgfile).to_s
1372
+ end
1373
+ end
1374
+
1375
+ spec "[!6j4fh] sets Last-Modified with file timestamp." do
1376
+ |action_obj, pngfile|
1377
+ expected = K8::Util.http_utc_time(File.mtime(pngfile).utc)
1378
+ action_obj.instance_exec(self) do |_|
1379
+ send_file(pngfile)
1380
+ _.ok {@resp.headers['Last-Modified']} == expected
1381
+ end
1382
+ end
1383
+
1384
+ spec "[!iblvb] raises 404 Not Found when file not exist." do
1385
+ |action_obj|
1386
+ action_obj.instance_exec(self) do |_|
1387
+ pr = proc { send_file('hom-hom.hom') }
1388
+ _.ok {pr}.raise?(K8::HttpException)
1389
+ _.ok {pr.exception.status_code} == 404
1390
+ end
1391
+ end
1392
+
1393
+ end
1394
+
1395
+
1396
+ end
1397
+
1398
+
1399
+ topic K8::DefaultPatterns do
1400
+
1401
+
1402
+ topic '#register()' do
1403
+
1404
+ spec "[!yfsom] registers urlpath param name, default pattern and converter block." do
1405
+ K8::DefaultPatterns.new.instance_exec(self) do |_|
1406
+ _.ok {@patterns.length} == 0
1407
+ register(/_id\z/, '\d+') {|x| x.to_i }
1408
+ _.ok {@patterns.length} == 1
1409
+ _.ok {@patterns[0][0]} == /_id\z/
1410
+ _.ok {@patterns[0][1]} == '\d+'
1411
+ _.ok {@patterns[0][2]}.is_a?(Proc)
1412
+ _.ok {@patterns[0][2].call("123")} == 123
1413
+ end
1414
+ end
1415
+
1416
+ end
1417
+
1418
+
1419
+ topic '#unregister()' do
1420
+
1421
+ spec "[!3gplv] deletes matched record." do
1422
+ K8::DefaultPatterns.new.instance_exec(self) do |_|
1423
+ register("id", '\d+') {|x| x.to_i }
1424
+ register(/_id\z/, '\d+') {|x| x.to_i }
1425
+ _.ok {@patterns.length} == 2
1426
+ unregister(/_id\z/)
1427
+ _.ok {@patterns.length} == 1
1428
+ _.ok {@patterns[0][0]} == "id"
1429
+ end
1430
+ end
1431
+
1432
+ end
1433
+
1434
+
1435
+ topic '#lookup()' do
1436
+
1437
+ spec "[!dvbqx] returns default pattern string and converter proc when matched." do
1438
+ K8::DefaultPatterns.new.instance_exec(self) do |_|
1439
+ register("id", '\d+') {|x| x.to_i }
1440
+ register(/_id\z/, '\d+') {|x| x.to_i }
1441
+ _.ok {lookup("id")}.is_a?(Array).length(2)
1442
+ _.ok {lookup("id")[0]} == '\d+'
1443
+ _.ok {lookup("id")[1].call("123")} == 123
1444
+ _.ok {lookup("book_id")[0]} == '\d+'
1445
+ _.ok {lookup("book_id")[1]}.is_a?(Proc)
1446
+ _.ok {lookup("book_id")[1].call("123")} == 123
1447
+ end
1448
+ end
1449
+
1450
+ spec "[!6hblo] returns '[^/]*?' and nil as default pattern and converter proc when nothing matched." do
1451
+ K8::DefaultPatterns.new.instance_exec(self) do |_|
1452
+ register("id", '\d+') {|x| x.to_i }
1453
+ register(/_id\z/, '\d+') {|x| x.to_i }
1454
+ _.ok {lookup("code")}.is_a?(Array).length(2)
1455
+ _.ok {lookup("code")[0]} == '[^/]+?'
1456
+ _.ok {lookup("code")[1]} == nil
1457
+ end
1458
+ end
1459
+
1460
+ end
1461
+
1462
+ end
1463
+
1464
+
1465
+ topic K8::ActionMethodMapping do
1466
+
1467
+ fixture :mapping do
1468
+ mapping = K8::ActionMethodMapping.new
1469
+ mapping.map '/', :GET=>:do_index, :POST=>:do_create
1470
+ mapping.map '/{id:\d+}', :GET=>:do_show, :PUT=>:do_update
1471
+ mapping
1472
+ end
1473
+
1474
+ fixture :methods1 do
1475
+ {:GET=>:do_index, :POST=>:do_create}
1476
+ end
1477
+
1478
+ fixture :methods2 do
1479
+ {:GET=>:do_show, :PUT=>:do_update}
1480
+ end
1481
+
1482
+
1483
+ topic '#map()' do
1484
+
1485
+ spec "[!s7cs9] maps urlpath and methods." do
1486
+ |mapping|
1487
+ arr = mapping.instance_variable_get('@mappings')
1488
+ ok {arr}.is_a?(Array)
1489
+ ok {arr.length} == 2
1490
+ ok {arr[0]} == ['/', {:GET=>:do_index, :POST=>:do_create}]
1491
+ ok {arr[1]} == ['/{id:\d+}', {:GET=>:do_show, :PUT=>:do_update}]
1492
+ end
1493
+
1494
+ spec "[!o6cxr] returns self." do
1495
+ |mapping|
1496
+ ok {mapping.map '/new', :GET=>:do_new}.same?(mapping)
1497
+ end
1498
+
1499
+ end
1500
+
1501
+
1502
+ topic '#each()' do
1503
+
1504
+ spec "[!62y5q] yields each urlpath pattern and action methods." do
1505
+ |mapping, methods1, methods2|
1506
+ arr = []
1507
+ mapping.each do |urlpath_pat, action_methods|
1508
+ arr << [urlpath_pat, action_methods]
1509
+ end
1510
+ ok {arr} == [
1511
+ ['/', methods1],
1512
+ ['/{id:\d+}', methods2],
1513
+ ]
1514
+ end
1515
+
1516
+ end
1517
+
1518
+
1519
+ end
1520
+
1521
+
1522
+ topic K8::ActionClassMapping do
1523
+
1524
+ fixture :mapping do
1525
+ K8::ActionClassMapping.new
1526
+ end
1527
+
1528
+ fixture :proc_obj1 do
1529
+ _, proc_obj = K8::DEFAULT_PATTERNS.lookup('id')
1530
+ proc_obj
1531
+ end
1532
+
1533
+ fixture :proc_obj2 do
1534
+ _, proc_obj = K8::DEFAULT_PATTERNS.lookup('book_id')
1535
+ proc_obj
1536
+ end
1537
+
1538
+ topic '#mount()' do
1539
+
1540
+ fixture :testapi_books do
1541
+ Dir.mkdir 'testapi' unless File.exist? 'testapi'
1542
+ at_end do
1543
+ Dir.glob('testapi/*').each {|f| File.unlink f }
1544
+ Dir.rmdir 'testapi'
1545
+ end
1546
+ File.open('testapi/books.rb', 'w') do |f|
1547
+ f << <<-'END'
1548
+ require 'keight'
1549
+ #
1550
+ class MyBooksAPI < K8::Action
1551
+ mapping '', :GET=>:do_index
1552
+ def do_index; ''; end
1553
+ class MyError < Exception
1554
+ end
1555
+ end
1556
+ #
1557
+ module Admin
1558
+ class Admin::BooksAPI < K8::Action
1559
+ mapping '', :GET=>:do_index
1560
+ def do_index; ''; end
1561
+ end
1562
+ end
1563
+ END
1564
+ end
1565
+ './testapi/books:BooksAction'
1566
+ end
1567
+
1568
+ spec "[!flb11] mounts action class to urlpath." do
1569
+ |mapping|
1570
+ mapping.mount '/books', BooksAction
1571
+ arr = mapping.instance_variable_get('@mappings')
1572
+ ok {arr}.is_a?(Array)
1573
+ ok {arr.length} == 1
1574
+ ok {arr[0]}.is_a?(Array)
1575
+ ok {arr[0].length} == 2
1576
+ ok {arr[0][0]} == '/books'
1577
+ ok {arr[0][1]} == BooksAction
1578
+ end
1579
+
1580
+ spec "[!4l8xl] can accept array of pairs of urlpath and action class." do
1581
+ |mapping|
1582
+ mapping.mount '/api', [
1583
+ ['/books', BooksAction],
1584
+ ]
1585
+ arr = mapping.instance_variable_get('@mappings')
1586
+ ok {arr} == [
1587
+ ['/api', [
1588
+ ['/books', BooksAction],
1589
+ ]],
1590
+ ]
1591
+ end
1592
+
1593
+ case_when "[!ne804] when target class name is string..." do
1594
+
1595
+ spec "[!9brqr] raises error when string format is invalid." do
1596
+ |mapping, testapi_books|
1597
+ pr = proc { mapping.mount '/books', 'books.MyBooksAPI' }
1598
+ ok {pr}.raise?(ArgumentError, "mount('books.MyBooksAPI'): expected 'file/path:ClassName'.")
1599
+ end
1600
+
1601
+ spec "[!jpg56] loads file." do
1602
+ |mapping, testapi_books|
1603
+ pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI' }
1604
+ ok {pr}.NOT.raise?(Exception)
1605
+ ok {MyBooksAPI}.is_a?(Class)
1606
+ end
1607
+
1608
+ spec "[!vaazw] raises error when failed to load file." do
1609
+ |mapping, testapi_books|
1610
+ pr = proc { mapping.mount '/books', './testapi/books999:MyBooksAPI' }
1611
+ ok {pr}.raise?(ArgumentError, "mount('./testapi/books999:MyBooksAPI'): failed to require file.")
1612
+ end
1613
+
1614
+ spec "[!eiovd] raises original LoadError when it raises in loading file." do
1615
+ |mapping, testapi_books|
1616
+ filepath = './testapi/books7.rb'
1617
+ ok {filepath}.NOT.exist?
1618
+ File.open(filepath, 'w') {|f| f << "require 'homhom7'\n" }
1619
+ pr = proc { mapping.mount '/books', './testapi/books7:MyBooks7API' }
1620
+ ok {pr}.raise?(LoadError, "cannot load such file -- homhom7")
1621
+ end
1622
+
1623
+ spec "[!au27n] finds target class." do
1624
+ |mapping, testapi_books|
1625
+ pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI' }
1626
+ ok {pr}.NOT.raise?(Exception)
1627
+ ok {MyBooksAPI}.is_a?(Class)
1628
+ ok {MyBooksAPI} < K8::Action
1629
+ #
1630
+ pr = proc { mapping.mount '/books', './testapi/books:Admin::BooksAPI' }
1631
+ ok {pr}.NOT.raise?(Exception)
1632
+ ok {Admin::BooksAPI}.is_a?(Class)
1633
+ ok {Admin::BooksAPI} < K8::Action
1634
+ end
1635
+
1636
+ spec "[!k9bpm] raises error when target class not found." do
1637
+ |mapping, testapi_books|
1638
+ pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI999' }
1639
+ ok {pr}.raise?(ArgumentError, "mount('./testapi/books:MyBooksAPI999'): no such action class.")
1640
+ end
1641
+
1642
+ spec "[!t6key] raises error when target class is not an action class." do
1643
+ |mapping, testapi_books|
1644
+ pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI::MyError' }
1645
+ ok {pr}.raise?(ArgumentError, "mount('./testapi/books:MyBooksAPI::MyError'): not an action class.")
1646
+ end
1647
+
1648
+ end
1649
+
1650
+ spec "[!lvxyx] raises error when not an action class." do
1651
+ |mapping|
1652
+ pr = proc { mapping.mount '/api', String }
1653
+ ok {pr}.raise?(ArgumentError, "mount('/api'): Action class expected but got: String")
1654
+ end
1655
+
1656
+ spec "[!w8mee] returns self." do
1657
+ |mapping|
1658
+ ret = mapping.mount '/books', BooksAction
1659
+ ok {ret}.same?(mapping)
1660
+ end
1661
+
1662
+ end
1663
+
1664
+
1665
+ topic '#traverse()' do
1666
+
1667
+ spec "[!ds0fp] yields with event (:enter, :map or :exit)." do
1668
+ mapping = K8::ActionClassMapping.new
1669
+ mapping.mount '/api', [
1670
+ ['/books', BooksAction],
1671
+ ['/books/{book_id}/comments', BookCommentsAction],
1672
+ ]
1673
+ mapping.mount '/admin', [
1674
+ ['/books', AdminBooksAction],
1675
+ ]
1676
+ #
1677
+ arr = []
1678
+ mapping.traverse do |*args|
1679
+ arr << args
1680
+ end
1681
+ ok {arr[0]} == [:enter, "", "/api", [["/books", BooksAction], ["/books/{book_id}/comments", BookCommentsAction]], nil]
1682
+ ok {arr[1]} == [:enter, "/api", "/books", BooksAction, nil]
1683
+ ok {arr[2]} == [:map, "/api/books", "/", BooksAction, {:GET=>:do_index, :POST=>:do_create}]
1684
+ ok {arr[3]} == [:map, "/api/books", "/new", BooksAction, {:GET=>:do_new}]
1685
+ ok {arr[4]} == [:map, "/api/books", "/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}]
1686
+ ok {arr[5]} == [:map, "/api/books", "/{id}/edit", BooksAction, {:GET=>:do_edit}]
1687
+ ok {arr[6]} == [:exit, "/api", "/books", BooksAction, nil]
1688
+ ok {arr[7]} == [:enter, "/api", "/books/{book_id}/comments", BookCommentsAction, nil]
1689
+ ok {arr[8]} == [:map, "/api/books/{book_id}/comments", "/comments", BookCommentsAction, {:GET=>:do_comments}]
1690
+ ok {arr[9]} == [:map, "/api/books/{book_id}/comments", "/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}]
1691
+ ok {arr[10]} == [:exit, "/api", "/books/{book_id}/comments", BookCommentsAction, nil]
1692
+ ok {arr[11]} == [:exit, "", "/api", [["/books", BooksAction], ["/books/{book_id}/comments", BookCommentsAction]], nil]
1693
+ ok {arr[12]} == [:enter, "", "/admin", [["/books", AdminBooksAction]], nil]
1694
+ ok {arr[13]} == [:enter, "/admin", "/books", AdminBooksAction, nil]
1695
+ ok {arr[14]} == [:map, "/admin/books", "/", AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}]
1696
+ ok {arr[15]} == [:map, "/admin/books", "/new", AdminBooksAction, {:GET=>:do_new}]
1697
+ ok {arr[16]} == [:map, "/admin/books", "/{id}", AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}]
1698
+ ok {arr[17]} == [:map, "/admin/books", "/{id}/edit", AdminBooksAction, {:GET=>:do_edit}]
1699
+ ok {arr[18]} == [:exit, "/admin", "/books", AdminBooksAction, nil]
1700
+ ok {arr[19]} == [:exit, "", "/admin", [["/books", AdminBooksAction]], nil]
1701
+ ok {arr[20]} == nil
1702
+ end
1703
+
1704
+ end
1705
+
1706
+
1707
+ topic '#each_mapping()' do
1708
+
1709
+ spec "[!driqt] yields full urlpath pattern, action class and action methods." do
1710
+ mapping = K8::ActionClassMapping.new
1711
+ mapping.mount '/api', [
1712
+ ['/books', BooksAction],
1713
+ ['/books/{book_id}', BookCommentsAction],
1714
+ ]
1715
+ mapping.mount '/admin', [
1716
+ ['/books', AdminBooksAction],
1717
+ ]
1718
+ #
1719
+ arr = []
1720
+ mapping.each_mapping do |*args|
1721
+ arr << args
1722
+ end
1723
+ ok {arr} == [
1724
+ ["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1725
+ ["/api/books/new", BooksAction, {:GET=>:do_new}],
1726
+ ["/api/books/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
1727
+ ["/api/books/{id}/edit", BooksAction, {:GET=>:do_edit}],
1728
+ #
1729
+ ["/api/books/{book_id}/comments", BookCommentsAction, {:GET=>:do_comments}],
1730
+ ["/api/books/{book_id}/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}],
1731
+ #
1732
+ ["/admin/books/", AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}],
1733
+ ["/admin/books/new", AdminBooksAction, {:GET=>:do_new}],
1734
+ ["/admin/books/{id}", AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
1735
+ ["/admin/books/{id}/edit", AdminBooksAction, {:GET=>:do_edit}],
1736
+ ]
1737
+ end
1738
+
1739
+ end
1740
+
1741
+
1742
+ end
1743
+
1744
+
1745
+ topic K8::ActionFinder do
1746
+
1747
+ fixture :router do |class_mapping, default_patterns|
1748
+ K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 0)
1749
+ end
1750
+
1751
+ fixture :class_mapping do
1752
+ mapping = K8::ActionClassMapping.new
1753
+ mapping.mount '/api', [
1754
+ ['/books', BooksAction],
1755
+ ['/books/{book_id}', BookCommentsAction],
1756
+ ]
1757
+ mapping.mount '/admin', [
1758
+ ['/books', AdminBooksAction],
1759
+ ]
1760
+ mapping
1761
+ end
1762
+
1763
+ fixture :default_patterns do |proc_obj1, proc_obj2|
1764
+ default_patterns = K8::DefaultPatterns.new
1765
+ default_patterns.register('id', '\d+', &proc_obj1)
1766
+ default_patterns.register(/_id\z/, '\d+', &proc_obj2)
1767
+ default_patterns
1768
+ end
1769
+
1770
+ fixture :proc_obj1 do
1771
+ proc {|x| x.to_i }
1772
+ end
1773
+
1774
+ fixture :proc_obj2 do
1775
+ proc {|x| x.to_i }
1776
+ end
1777
+
1778
+
1779
+ topic '#initialize()' do
1780
+
1781
+ spec "[!dnu4q] calls '#_construct()'." do
1782
+ |router|
1783
+ ok {router.instance_variable_get('@rexp')} != nil
1784
+ ok {router.instance_variable_get('@list')} != nil
1785
+ ok {router.instance_variable_get('@dict')} != nil
1786
+ end
1787
+
1788
+ spec "[!wb9l8] enables urlpath cache when urlpath_cache_size > 0." do
1789
+ |class_mapping, default_patterns|
1790
+ args = [class_mapping, default_patterns]
1791
+ router = K8::ActionFinder.new(*args, urlpath_cache_size: 1)
1792
+ ok {router.instance_variable_get('@urlpath_cache')} == {}
1793
+ router = K8::ActionFinder.new(*args, urlpath_cache_size: 0)
1794
+ ok {router.instance_variable_get('@urlpath_cache')} == nil
1795
+ end
1796
+
1797
+ end
1798
+
1799
+
1800
+ topic '#_compile()' do
1801
+
1802
+ spec "[!izsbp] compiles urlpath pattern into regexp string and param names." do
1803
+ |router, proc_obj1|
1804
+ router.instance_exec(self) do |_|
1805
+ ret = _compile('/', '\A', '\z', true)
1806
+ _.ok {ret} == ['\A/\z', [], []]
1807
+ ret = _compile('/books', '\A', '\z', true)
1808
+ _.ok {ret} == ['\A/books\z', [], []]
1809
+ ret = _compile('/books/{id:\d*}', '\A', '\z', true)
1810
+ _.ok {ret} == ['\A/books/(\d*)\z', ["id"], [nil]]
1811
+ ret = _compile('/books/{id}/authors/{name}', '\A', '\z', true)
1812
+ _.ok {ret} == ['\A/books/(\d+)/authors/([^/]+?)\z', ["id", "name"], [proc_obj1, nil]]
1813
+ end
1814
+ end
1815
+
1816
+ spec "[!olps9] allows '{}' in regular expression." do
1817
+ |router|
1818
+ router.instance_exec(self) do |_|
1819
+ ret = _compile('/log/{date:\d{4}-\d{2}-\d{2}}', '', '', true)
1820
+ _.ok {ret} == ['/log/(\d{4}-\d{2}-\d{2})', ["date"], [nil]]
1821
+ end
1822
+ end
1823
+
1824
+ spec "[!vey08] uses grouping when 4th argument is true." do
1825
+ |router, proc_obj1|
1826
+ router.instance_exec(self) do |_|
1827
+ ret = _compile('/books/{id:\d*}', '\A', '\z', true)
1828
+ _.ok {ret} == ['\A/books/(\d*)\z', ["id"], [nil]]
1829
+ ret = _compile('/books/{id}/authors/{name}', '\A', '\z', true)
1830
+ _.ok {ret} == ['\A/books/(\d+)/authors/([^/]+?)\z', ["id", "name"], [proc_obj1, nil]]
1831
+ end
1832
+ end
1833
+
1834
+ spec "[!2zil2] don't use grouping when 4th argument is false." do
1835
+ |router, proc_obj1|
1836
+ router.instance_exec(self) do |_|
1837
+ ret = _compile('/books/{id:\d*}', '\A', '\z', false)
1838
+ _.ok {ret} == ['\A/books/\d*\z', ["id"], [nil]]
1839
+ ret = _compile('/books/{id}/authors/{name}', '\A', '\z', false)
1840
+ _.ok {ret} == ['\A/books/\d+/authors/[^/]+?\z', ["id", "name"], [proc_obj1, nil]]
1841
+ end
1842
+ end
1843
+
1844
+ spec %q"[!rda92] ex: '/{id:\d+}' -> '/(\d+)'" do
1845
+ |router|
1846
+ router.instance_exec(self) do |_|
1847
+ ret = _compile('/api/{ver:\d+}', '', '', true)
1848
+ _.ok {ret} == ['/api/(\d+)', ["ver"], [nil]]
1849
+ end
1850
+ end
1851
+
1852
+ spec %q"[!jyz2g] ex: '/{:\d+}' -> '/\d+'" do
1853
+ |router|
1854
+ router.instance_exec(self) do |_|
1855
+ ret = _compile('/api/{:\d+}', '', '', true)
1856
+ _.ok {ret} == ['/api/\d+', [], []]
1857
+ end
1858
+ end
1859
+
1860
+ spec %q"[!hy3y5] ex: '/{:xx|yy}' -> '/(?:xx|yy)'" do
1861
+ |router|
1862
+ router.instance_exec(self) do |_|
1863
+ ret = _compile('/api/{:2014|2015}', '', '', true)
1864
+ _.ok {ret} == ['/api/(?:2014|2015)', [], []]
1865
+ end
1866
+ end
1867
+
1868
+ spec %q"[!gunsm] ex: '/{id:xx|yy}' -> '/(xx|yy)'" do
1869
+ |router|
1870
+ router.instance_exec(self) do |_|
1871
+ ret = _compile('/api/{year:2014|2015}', '', '', true)
1872
+ _.ok {ret} == ['/api/(2014|2015)', ["year"], [nil]]
1873
+ end
1874
+ end
1875
+
1876
+ end
1877
+
1878
+
1879
+ topic '#_construct()' do
1880
+
1881
+ spec "[!956fi] builds regexp object for variable urlpaths (= containing urlpath params)." do
1882
+ |router|
1883
+ rexp = router.instance_variable_get('@rexp')
1884
+ ok {rexp}.is_a?(Regexp)
1885
+ ok {rexp.source} == '
1886
+ \A
1887
+ (?:
1888
+ /api
1889
+ (?:
1890
+ /books
1891
+ (?: /\d+(\z) | /\d+/edit(\z) )
1892
+ |
1893
+ /books/\d+
1894
+ (?: /comments(\z) | /comments/\d+(\z) )
1895
+ )
1896
+ |
1897
+ /admin
1898
+ (?:
1899
+ /books
1900
+ (?: /\d+(\z) | /\d+/edit(\z) )
1901
+ )
1902
+ )
1903
+ '.gsub(/\s+/, '')
1904
+ end
1905
+
1906
+ spec "[!6tgj5] builds dict of fixed urlpaths (= no urlpath params)." do
1907
+ |router|
1908
+ dict = router.instance_variable_get('@dict')
1909
+ ok {dict} == {
1910
+ '/api/books/' => [BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1911
+ '/api/books/new' => [BooksAction, {:GET=>:do_new}],
1912
+ '/admin/books/' => [AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}],
1913
+ '/admin/books/new' => [AdminBooksAction, {:GET=>:do_new}],
1914
+ }
1915
+ end
1916
+
1917
+ spec "[!sl9em] builds list of variable urlpaths (= containing urlpath params)." do
1918
+ |router, proc_obj1, proc_obj2|
1919
+ list = router.instance_variable_get('@list')
1920
+ ok {list}.is_a?(Array)
1921
+ ok {list.length} == 6
1922
+ ok {list[0]} == [
1923
+ /\A\/api\/books\/(\d+)\z/,
1924
+ ["id"], [proc_obj1],
1925
+ BooksAction,
1926
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1927
+ ]
1928
+ ok {list[1]} == [
1929
+ /\A\/api\/books\/(\d+)\/edit\z/,
1930
+ ["id"], [proc_obj1],
1931
+ BooksAction,
1932
+ {:GET=>:do_edit},
1933
+ ]
1934
+ ok {list[2]} == [
1935
+ /\A\/api\/books\/(\d+)\/comments\z/,
1936
+ ["book_id"], [proc_obj2],
1937
+ BookCommentsAction,
1938
+ {:GET=>:do_comments},
1939
+ ]
1940
+ ok {list[3]} == [
1941
+ /\A\/api\/books\/(\d+)\/comments\/(\d+)\z/,
1942
+ ["book_id", "comment_id"], [proc_obj2, proc_obj2],
1943
+ BookCommentsAction,
1944
+ {:GET=>:do_comment},
1945
+ ]
1946
+ ok {list[4]} == [
1947
+ /\A\/admin\/books\/(\d+)\z/,
1948
+ ["id"], [proc_obj1],
1949
+ AdminBooksAction,
1950
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1951
+ ]
1952
+ ok {list[5]} == [
1953
+ /\A\/admin\/books\/(\d+)\/edit\z/,
1954
+ ["id"], [proc_obj1],
1955
+ AdminBooksAction,
1956
+ {:GET=>:do_edit},
1957
+ ]
1958
+ ok {list[6]} == nil
1959
+ end
1960
+
1961
+ end
1962
+
1963
+
1964
+ topic '#find()' do
1965
+
1966
+ spec "[!ndktw] returns action class, action methods, urlpath names and values." do
1967
+ |router|
1968
+ ok {router.find('/api/books/')} == [
1969
+ BooksAction, {:GET=>:do_index, :POST=>:do_create}, [], [],
1970
+ ]
1971
+ ok {router.find('/api/books/123')} == [
1972
+ BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ["id"], [123],
1973
+ ]
1974
+ end
1975
+
1976
+ spec "[!p18w0] urlpath params are empty when matched to fixed urlpath pattern." do
1977
+ |router|
1978
+ ok {router.find('/admin/books/')} == [
1979
+ AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}, [], [],
1980
+ ]
1981
+ end
1982
+
1983
+ spec "[!t6yk0] urlpath params are not empty when matched to variable urlpath apttern." do
1984
+ |router|
1985
+ ok {router.find('/admin/books/123')} == [
1986
+ AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ["id"], [123],
1987
+ ]
1988
+ ok {router.find('/api/books/123/comments/999')} == [
1989
+ BookCommentsAction, {:GET=>:do_comment}, ["book_id", "comment_id"], [123, 999],
1990
+ ]
1991
+ end
1992
+
1993
+ spec "[!0o3fe] converts urlpath param values according to default patterns." do
1994
+ |router|
1995
+ ok {router.find('/api/books/123')[-1]} == [123]
1996
+ ok {router.find('/api/books/123/comments/999')[-1]} == [123, 999]
1997
+ end
1998
+
1999
+ spec "[!ps5jm] returns nil when not matched to any urlpath patterns." do
2000
+ |router|
2001
+ ok {router.find('/admin/authors')} == nil
2002
+ end
2003
+
2004
+ spec "[!gzy2w] fetches variable urlpath from LRU cache if LRU cache is enabled." do
2005
+ |class_mapping, default_patterns|
2006
+ router = K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 3)
2007
+ router.instance_exec(self) do |_|
2008
+ arr1 = find('/api/books/1')
2009
+ arr2 = find('/api/books/2')
2010
+ arr3 = find('/api/books/3')
2011
+ _.ok {@urlpath_cache.keys} == ['/api/books/1', '/api/books/2', '/api/books/3']
2012
+ #
2013
+ _.ok {find('/api/books/2')} == arr2
2014
+ _.ok {@urlpath_cache.keys} == ['/api/books/1', '/api/books/3', '/api/books/2']
2015
+ _.ok {find('/api/books/1')} == arr1
2016
+ _.ok {@urlpath_cache.keys} == ['/api/books/3', '/api/books/2', '/api/books/1']
2017
+ end
2018
+ end
2019
+
2020
+ spec "[!v2zbx] caches variable urlpath into LRU cache if cache is enabled." do
2021
+ |class_mapping, default_patterns|
2022
+ router = K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 3)
2023
+ router.instance_exec(self) do |_|
2024
+ arr1 = find('/api/books/1')
2025
+ arr2 = find('/api/books/2')
2026
+ _.ok {@urlpath_cache.keys} == ['/api/books/1', '/api/books/2']
2027
+ _.ok {find('/api/books/1')} == arr1
2028
+ _.ok {find('/api/books/2')} == arr2
2029
+ end
2030
+ end
2031
+
2032
+ spec "[!nczw6] LRU cache size doesn't growth over max cache size." do
2033
+ |class_mapping, default_patterns|
2034
+ router = K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 3)
2035
+ router.instance_exec(self) do |_|
2036
+ arr1 = find('/api/books/1')
2037
+ arr2 = find('/api/books/2')
2038
+ arr3 = find('/api/books/3')
2039
+ arr3 = find('/api/books/4')
2040
+ arr3 = find('/api/books/5')
2041
+ _.ok {@urlpath_cache.length} == 3
2042
+ _.ok {@urlpath_cache.keys} == ['/api/books/3', '/api/books/4', '/api/books/5']
2043
+ end
2044
+ end
2045
+
2046
+ end
2047
+
2048
+
2049
+ end
2050
+
2051
+
2052
+ topic K8::ActionRouter do
2053
+
2054
+
2055
+ topic '#initialize()' do
2056
+
2057
+ spec "[!l1elt] saves finder options." do
2058
+ router = K8::ActionRouter.new(urlpath_cache_size: 100)
2059
+ router.instance_exec(self) do |_|
2060
+ _.ok {@finder_opts} == {:urlpath_cache_size=>100}
2061
+ end
2062
+ end
2063
+
2064
+ end
2065
+
2066
+
2067
+ topic '#register()' do
2068
+
2069
+ spec "[!boq80] registers urlpath param pattern and converter." do
2070
+ router = K8::ActionRouter.new()
2071
+ router.register(/_hex\z/, '[a-f0-9]+') {|x| x.to_i(16) }
2072
+ router.instance_exec(self) do |_|
2073
+ ret = @default_patterns.lookup('code_hex')
2074
+ _.ok {ret.length} == 2
2075
+ _.ok {ret[0]} == '[a-f0-9]+'
2076
+ _.ok {ret[1]}.is_a?(Proc)
2077
+ _.ok {ret[1].call('ff')} == 255
2078
+ end
2079
+ end
2080
+
2081
+ end
2082
+
2083
+
2084
+ topic '#mount()' do
2085
+
2086
+ spec "[!uc996] mouts action class to urlpath." do
2087
+ router = K8::ActionRouter.new()
2088
+ router.mount('/api/books', BooksAction)
2089
+ ret = router.find('/api/books/')
2090
+ ok {ret} != nil
2091
+ ok {ret[0]} == BooksAction
2092
+ end
2093
+
2094
+ spec "[!trs6w] removes finder object." do
2095
+ router = K8::ActionRouter.new()
2096
+ router.instance_exec(self) do |_|
2097
+ @finder = true
2098
+ _.ok {@finder} == true
2099
+ mount('/api/books', BooksAction)
2100
+ _.ok {@finder} == nil
2101
+ end
2102
+ end
2103
+
2104
+ end
2105
+
2106
+
2107
+ topic '#each_mapping()' do
2108
+
2109
+ spec "[!2kq9h] yields with full urlpath pattern, action class and action methods." do
2110
+ router = K8::ActionRouter.new()
2111
+ router.mount '/api', [
2112
+ ['/books', BooksAction],
2113
+ ['/books/{book_id}', BookCommentsAction],
2114
+ ]
2115
+ router.mount '/admin', [
2116
+ ['/books', AdminBooksAction],
2117
+ ]
2118
+ arr = []
2119
+ router.each_mapping do |*args|
2120
+ arr << args
2121
+ end
2122
+ ok {arr} == [
2123
+ ["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
2124
+ ["/api/books/new", BooksAction, {:GET=>:do_new}],
2125
+ ["/api/books/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
2126
+ ["/api/books/{id}/edit", BooksAction, {:GET=>:do_edit}],
2127
+ ["/api/books/{book_id}/comments", BookCommentsAction, {:GET=>:do_comments}],
2128
+ ["/api/books/{book_id}/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}],
2129
+ ["/admin/books/", AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}],
2130
+ ["/admin/books/new", AdminBooksAction, {:GET=>:do_new}],
2131
+ ["/admin/books/{id}", AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
2132
+ ["/admin/books/{id}/edit", AdminBooksAction, {:GET=>:do_edit}],
2133
+ ]
2134
+ end
2135
+
2136
+ end
2137
+
2138
+
2139
+ topic '#find()' do
2140
+
2141
+ spec "[!zsuzg] creates finder object automatically if necessary." do
2142
+ router = K8::ActionRouter.new(urlpath_cache_size: 99)
2143
+ router.mount '/api/books', BooksAction
2144
+ router.instance_exec(self) do |_|
2145
+ _.ok {@finder} == nil
2146
+ find('/api/books/123')
2147
+ _.ok {@finder} != nil
2148
+ _.ok {@finder}.is_a?(K8::ActionFinder)
2149
+ end
2150
+ end
2151
+
2152
+ spec "[!9u978] urlpath_cache_size keyword argument will be passed to router oubject." do
2153
+ router = K8::ActionRouter.new(urlpath_cache_size: 99)
2154
+ router.mount '/api/books', BooksAction
2155
+ router.instance_exec(self) do |_|
2156
+ find('/api/books/123')
2157
+ _.ok {@finder.instance_variable_get('@urlpath_cache_size')} == 99
2158
+ end
2159
+ end
2160
+
2161
+ spec "[!m9klu] returns action class, action methods, urlpath param names and values." do
2162
+ router = K8::ActionRouter.new(urlpath_cache_size: 99)
2163
+ router.register('id', '\d+') {|x| x.to_i }
2164
+ router.mount '/api', [
2165
+ ['/books', BooksAction],
2166
+ ['/books/{book_id}', BookCommentsAction],
2167
+ ]
2168
+ router.mount '/admin', [
2169
+ ['/books', AdminBooksAction],
2170
+ ]
2171
+ ret = router.find('/admin/books/123')
2172
+ ok {ret} == [
2173
+ AdminBooksAction,
2174
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
2175
+ ["id"],
2176
+ [123],
2177
+ ]
2178
+ end
2179
+
2180
+ end
2181
+
2182
+
2183
+ end
2184
+
2185
+
2186
+ topic K8::RackApplication do
2187
+
2188
+ fixture :app do
2189
+ app = K8::RackApplication.new
2190
+ app.mount '/api', [
2191
+ ['/books', BooksAction],
2192
+ ]
2193
+ app
2194
+ end
2195
+
2196
+
2197
+ topic '#initialize()' do
2198
+
2199
+ spec "[!vkp65] mounts urlpath mappings if provided." do
2200
+ mapping = [
2201
+ ['/books' , BooksAction],
2202
+ ['/books/{id}/comments' , BookCommentsAction],
2203
+ ]
2204
+ app = K8::RackApplication.new(mapping)
2205
+ expected = <<-'END'
2206
+ - urlpath: /books/
2207
+ class: BooksAction
2208
+ methods: {GET: do_index, POST: do_create}
2209
+
2210
+ - urlpath: /books/new
2211
+ class: BooksAction
2212
+ methods: {GET: do_new}
2213
+
2214
+ - urlpath: /books/{id}
2215
+ class: BooksAction
2216
+ methods: {GET: do_show, PUT: do_update, DELETE: do_delete}
2217
+
2218
+ - urlpath: /books/{id}/edit
2219
+ class: BooksAction
2220
+ methods: {GET: do_edit}
2221
+
2222
+ - urlpath: /books/{id}/comments/comments
2223
+ class: BookCommentsAction
2224
+ methods: {GET: do_comments}
2225
+
2226
+ - urlpath: /books/{id}/comments/comments/{comment_id}
2227
+ class: BookCommentsAction
2228
+ methods: {GET: do_comment}
2229
+
2230
+ END
2231
+ expected.gsub!(/^ /, '')
2232
+ ok {app.show_mappings()} == expected
2233
+ end
2234
+
2235
+ end
2236
+
2237
+
2238
+ topic '#init_default_param_patterns()' do
2239
+
2240
+ spec "[!i51id] registers '\d+' as default pattern of param 'id' or /_id\z/." do
2241
+ |app|
2242
+ app.instance_exec(self) do |_|
2243
+ pat, proc_ = @router.default_patterns.lookup('id')
2244
+ _.ok {pat} == '\d+'
2245
+ _.ok {proc_.call("123")} == 123
2246
+ pat, proc_ = @router.default_patterns.lookup('book_id')
2247
+ _.ok {pat} == '\d+'
2248
+ _.ok {proc_.call("123")} == 123
2249
+ end
2250
+ end
2251
+
2252
+ spec "[!2g08b] registers '(?:\.\w+)?' as default pattern of param 'ext'." do
2253
+ |app|
2254
+ app.instance_exec(self) do |_|
2255
+ pat, proc_ = @router.default_patterns.lookup('ext')
2256
+ _.ok {pat} == '(?:\.\w+)?'
2257
+ _.ok {proc_} == nil
2258
+ end
2259
+ end
2260
+
2261
+ spec "[!8x5mp] registers '\d\d\d\d-\d\d-\d\d' as default pattern of param 'date' or /_date\z/." do
2262
+ |app|
2263
+ app.instance_exec(self) do |_|
2264
+ pat, proc_ = @router.default_patterns.lookup('date')
2265
+ _.ok {pat} == '\d\d\d\d-\d\d-\d\d'
2266
+ _.ok {proc_.call("2014-12-24")} == Date.new(2014, 12, 24)
2267
+ pat, proc_ = @router.default_patterns.lookup('birth_date')
2268
+ _.ok {pat} == '\d\d\d\d-\d\d-\d\d'
2269
+ _.ok {proc_.call("2015-02-14")} == Date.new(2015, 2, 14)
2270
+ end
2271
+ end
2272
+
2273
+ spec "[!wg9vl] raises 404 error when invalid date (such as 2012-02-30)." do
2274
+ |app|
2275
+ app.instance_exec(self) do |_|
2276
+ pat, proc_ = @router.default_patterns.lookup('date')
2277
+ pr = proc { proc_.call('2012-02-30') }
2278
+ _.ok {pr}.raise?(K8::HttpException, "2012-02-30: invalid date.")
2279
+ _.ok {pr.exception.status_code} == 404
2280
+ end
2281
+ end
2282
+
2283
+ end
2284
+
2285
+
2286
+ topic '#mount()' do
2287
+
2288
+ spec "[!zwva6] mounts action class to urlpath pattern." do
2289
+ |app|
2290
+ app.mount('/admin/books', AdminBooksAction)
2291
+ ret = app.find('/admin/books/123')
2292
+ ok {ret} == [
2293
+ AdminBooksAction,
2294
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
2295
+ ["id"],
2296
+ [123],
2297
+ ]
2298
+ end
2299
+
2300
+ end
2301
+
2302
+
2303
+ topic '#find()' do
2304
+
2305
+ spec "[!o0rnr] returns action class, action methods, urlpath names and values." do
2306
+ |app|
2307
+ ret = app.find('/api/books/')
2308
+ ok {ret} == [BooksAction, {:GET=>:do_index, :POST=>:do_create}, [], []]
2309
+ ret = app.find('/api/books/123')
2310
+ ok {ret} == [BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ["id"], [123]]
2311
+ end
2312
+
2313
+ end
2314
+
2315
+
2316
+ topic '#call()' do
2317
+
2318
+ spec "[!uvmxe] takes env object." do
2319
+ |app|
2320
+ env = new_env("GET", "/api/books/")
2321
+ pr = proc {app.call(env)}
2322
+ ok {pr}.NOT.raise?(Exception)
2323
+ end
2324
+
2325
+ spec "[!gpe4g] returns status, headers and content." do
2326
+ |app|
2327
+ env = new_env("GET", "/api/books/")
2328
+ ret = app.call(env)
2329
+ ok {ret}.is_a?(Array)
2330
+ status, headers, body = ret
2331
+ ok {status} == 200
2332
+ ok {headers} == {
2333
+ "Content-Type"=>"text/html; charset=utf-8",
2334
+ "Content-Length"=>"7",
2335
+ }
2336
+ ok {body} == ["<index>"]
2337
+ end
2338
+
2339
+ end
2340
+
2341
+
2342
+ topic '#handle_request()' do
2343
+
2344
+ spec "[!0fgbd] finds action class and invokes action method with urlpath params." do
2345
+ |app|
2346
+ env = new_env("GET", "/api/books/123")
2347
+ app.instance_exec(self) do |_|
2348
+ tuple = handle_request(K8::Request.new(env), K8::Response.new)
2349
+ _.ok {tuple}.is_a?(Array)
2350
+ status, headers, body = tuple
2351
+ _.ok {status} == 200
2352
+ _.ok {body} == ["<show:123(Fixnum)>"]
2353
+ _.ok {headers} == {
2354
+ "Content-Length" => "18",
2355
+ "Content-Type" => "text/html; charset=utf-8",
2356
+ }
2357
+ end
2358
+ end
2359
+
2360
+ spec "[!l6kmc] uses 'GET' method to find action when request method is 'HEAD'." do
2361
+ |app|
2362
+ env = new_env("HEAD", "/api/books/123")
2363
+ app.instance_exec(self) do |_|
2364
+ tuple = handle_request(K8::Request.new(env), K8::Response.new)
2365
+ status, headers, body = tuple
2366
+ _.ok {status} == 200
2367
+ _.ok {body} == [""]
2368
+ _.ok {headers} == {
2369
+ "Content-Length" => "18",
2370
+ "Content-Type" => "text/html; charset=utf-8",
2371
+ }
2372
+ end
2373
+ end
2374
+
2375
+ spec "[!4vmd3] uses '_method' value of query string as request method when 'POST' method." do
2376
+ |app|
2377
+ env = new_env("POST", "/api/books/123", query: {"_method"=>"DELETE"})
2378
+ app.instance_exec(self) do |_|
2379
+ tuple = handle_request(K8::Request.new(env), K8::Response.new)
2380
+ status, headers, body = tuple
2381
+ _.ok {status} == 200
2382
+ _.ok {body} == ["<delete:123(Fixnum)>"] # do_delete() caled
2383
+ end
2384
+ end
2385
+
2386
+ spec "[!vdllr] clears request and response if possible." do
2387
+ |app|
2388
+ req = K8::Request.new(new_env("GET", "/"))
2389
+ resp = K8::Response.new()
2390
+ req_clear = false
2391
+ (class << req; self; end).__send__(:define_method, :clear) { req_clear = true }
2392
+ resp_clear = false
2393
+ (class << resp; self; end).__send__(:define_method, :clear) { resp_clear = true }
2394
+ #
2395
+ app.instance_exec(self) do |_|
2396
+ tuple = handle_request(req, resp)
2397
+ _.ok {req_clear} == true
2398
+ _.ok {resp_clear} == true
2399
+ end
2400
+ end
2401
+
2402
+ spec "[!9wp9z] returns empty body when request method is HEAD." do
2403
+ |app|
2404
+ env = new_env("HEAD", "/api/books/123")
2405
+ app.instance_exec(self) do |_|
2406
+ tuple = handle_request(K8::Request.new(env), K8::Response.new)
2407
+ status, headers, body = tuple
2408
+ _.ok {body} == [""]
2409
+ end
2410
+ end
2411
+
2412
+ spec "[!rz13i] returns HTTP 404 when urlpath not found." do
2413
+ |app|
2414
+ env = new_env("GET", "/api/book/comments")
2415
+ app.instance_exec(self) do |_|
2416
+ tuple = handle_request(K8::Request.new(env), K8::Response.new)
2417
+ status, headers, body = tuple
2418
+ _.ok {status} == 404
2419
+ _.ok {body} == ["<div>\n<h2>404 Not Found</h2>\n<p></p>\n</div>\n"]
2420
+ _.ok {headers} == {
2421
+ "Content-Length" => "44",
2422
+ "Content-Type" => "text/html;charset=utf-8",
2423
+ }
2424
+ end
2425
+ end
2426
+
2427
+ spec "[!rv3cf] returns HTTP 405 when urlpath found but request method not allowed." do
2428
+ |app|
2429
+ env = new_env("POST", "/api/books/123")
2430
+ app.instance_exec(self) do |_|
2431
+ tuple = handle_request(K8::Request.new(env), K8::Response.new)
2432
+ status, headers, body = tuple
2433
+ _.ok {status} == 405
2434
+ _.ok {body} == ["<div>\n<h2>405 Method Not Allowed</h2>\n<p></p>\n</div>\n"]
2435
+ _.ok {headers} == {
2436
+ "Content-Length" => "53",
2437
+ "Content-Type" => "text/html;charset=utf-8",
2438
+ }
2439
+ end
2440
+ end
2441
+
2442
+
2443
+ end
2444
+
2445
+
2446
+ topic '#each_mapping()' do
2447
+
2448
+ spec "[!cgjyv] yields full urlpath pattern, action class and action methods." do
2449
+ |app|
2450
+ arr = []
2451
+ app.each_mapping do |*args|
2452
+ arr << args
2453
+ end
2454
+ ok {arr} == [
2455
+ ["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
2456
+ ["/api/books/new", BooksAction, {:GET=>:do_new}],
2457
+ ["/api/books/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
2458
+ ["/api/books/{id}/edit", BooksAction, {:GET=>:do_edit}],
2459
+ ]
2460
+ end
2461
+
2462
+ end
2463
+
2464
+
2465
+ topic '#show_mappings()' do
2466
+
2467
+ spec "[!u1g77] returns all mappings as YAML string." do
2468
+ |app|
2469
+ yaml_str = <<-'END'
2470
+ - urlpath: /api/books/
2471
+ class: BooksAction
2472
+ methods: {GET: do_index, POST: do_create}
2473
+
2474
+ - urlpath: /api/books/new
2475
+ class: BooksAction
2476
+ methods: {GET: do_new}
2477
+
2478
+ - urlpath: /api/books/{id}
2479
+ class: BooksAction
2480
+ methods: {GET: do_show, PUT: do_update, DELETE: do_delete}
2481
+
2482
+ - urlpath: /api/books/{id}/edit
2483
+ class: BooksAction
2484
+ methods: {GET: do_edit}
2485
+
2486
+ END
2487
+ yaml_str.gsub!(/^ /, '')
2488
+ ok {app.show_mappings()} == yaml_str
2489
+ end
2490
+
2491
+ end
2492
+
2493
+
2494
+ end
2495
+
2496
+
2497
+ topic K8::SecretValue do
2498
+
2499
+
2500
+ topic '#initialize()' do
2501
+
2502
+ spec "[!fbwnh] takes environment variable name." do
2503
+ obj = K8::SecretValue.new('DB_PASS')
2504
+ ok {obj.name} == 'DB_PASS'
2505
+ end
2506
+
2507
+ end
2508
+
2509
+
2510
+ topic '#value()' do
2511
+
2512
+ spec "[!gg06v] returns environment variable value." do
2513
+ obj = K8::SecretValue.new('TEST_HOMHOM')
2514
+ ok {obj.value} == nil
2515
+ ENV['TEST_HOMHOM'] = 'homhom'
2516
+ ok {obj.value} == 'homhom'
2517
+ end
2518
+
2519
+ end
2520
+
2521
+
2522
+ topic '#to_s()' do
2523
+
2524
+ spec "[!7ymqq] returns '<SECRET>' string when name not eixst." do
2525
+ ok {K8::SecretValue.new.to_s} == "<SECRET>"
2526
+ end
2527
+
2528
+ spec "[!x6edf] returns 'ENV[<name>]' string when name exists." do
2529
+ ok {K8::SecretValue.new('DB_PASS').to_s} == "ENV['DB_PASS']"
2530
+ end
2531
+
2532
+ end
2533
+
2534
+
2535
+ topic '#inspect()' do
2536
+
2537
+ spec "[!j27ji] 'inspect()' is alias of 'to_s()'." do
2538
+ ok {K8::SecretValue.new('DB_PASS').inspect} == "ENV['DB_PASS']"
2539
+ end
2540
+
2541
+ end
2542
+
2543
+
2544
+ topic '#[](name)' do
2545
+
2546
+ spec "[!jjqmn] creates new instance object with name." do
2547
+ obj = K8::SecretValue.new['DB_PASSWORD']
2548
+ ok {obj}.is_a?(K8::SecretValue)
2549
+ ok {obj.name} == 'DB_PASSWORD'
2550
+ end
2551
+
2552
+ end
2553
+
2554
+
2555
+ end
2556
+
2557
+
2558
+ topic K8::BaseConfig do
2559
+
2560
+
2561
+ topic '#initialize()' do
2562
+
2563
+ spec "[!vvd1n] copies key and values from class object." do
2564
+ class C01 < K8::BaseConfig
2565
+ add :haruhi , 'C' , "Suzumiya"
2566
+ add :mikuru , 'E' , "Asahina"
2567
+ add :yuki , 'A' , "Nagato"
2568
+ end
2569
+ c = C01.new
2570
+ c.instance_exec(self) do |_|
2571
+ _.ok {@haruhi} == 'C'
2572
+ _.ok {@mikuru} == 'E'
2573
+ _.ok {@yuki} == 'A'
2574
+ end
2575
+ end
2576
+
2577
+ spec "[!6dilv] freezes self and class object if 'freeze:' is true." do
2578
+ class C02 < K8::BaseConfig
2579
+ add :haruhi , 'C' , "Suzumiya"
2580
+ add :mikuru , 'E' , "Asahina"
2581
+ add :yuki , 'A' , "Nagato"
2582
+ end
2583
+ ## when freeze: false
2584
+ c = C02.new(freeze: false)
2585
+ pr = proc { c.instance_variable_set('@yuki', 'B') }
2586
+ ok {pr}.NOT.raise?(Exception)
2587
+ pr = proc { C02.class_eval { put :yuki, 'B' } }
2588
+ ok {pr}.NOT.raise?(Exception)
2589
+ ## when freeze: true
2590
+ c = C02.new
2591
+ pr = proc { c.instance_variable_set('@yuki', 'B') }
2592
+ ok {pr}.raise?(RuntimeError, "can't modify frozen C02")
2593
+ pr = proc { C02.class_eval { put :yuki, 'B' } }
2594
+ ok {pr}.raise?(RuntimeError, "can't modify frozen class")
2595
+ end
2596
+
2597
+ case_when "[!xok12] when value is SECRET..." do
2598
+
2599
+ spec "[!a4a4p] raises error when key not specified." do
2600
+ class C03 < K8::BaseConfig
2601
+ add :db_pass , SECRET, "db password"
2602
+ end
2603
+ pr = proc { C03.new }
2604
+ ok {pr}.raise?(K8::ConfigError, "config 'db_pass' should be set, but not.")
2605
+ end
2606
+
2607
+ spec "[!w4yl7] raises error when ENV value not specified." do
2608
+ class C04 < K8::BaseConfig
2609
+ add :db_pass1 , SECRET['DB_PASS1'], "db password"
2610
+ end
2611
+ ok {ENV['DB_PASS1']} == nil
2612
+ pr = proc { C04.new }
2613
+ ok {pr}.raise?(K8::ConfigError, )
2614
+ end
2615
+
2616
+ spec "[!he20d] get value from ENV." do
2617
+ class C05 < K8::BaseConfig
2618
+ add :db_pass1 , SECRET['DB_PASS1'], "db password"
2619
+ end
2620
+ begin
2621
+ ENV['DB_PASS1'] = 'homhom'
2622
+ pr = proc { C05.new }
2623
+ ok {pr}.NOT.raise?(Exception)
2624
+ ok {C05.new.db_pass1} == 'homhom'
2625
+ ensure
2626
+ ENV['DB_PASS1'] = nil
2627
+ end
2628
+ end
2629
+
2630
+ end
2631
+
2632
+ end
2633
+
2634
+
2635
+ topic '.has?()' do
2636
+
2637
+ spec "[!dv87n] returns true iff key is set." do
2638
+ class C11 < K8::BaseConfig
2639
+ @result1 = has? :foo
2640
+ put :foo, 1
2641
+ @result2 = has? :foo
2642
+ end
2643
+ ok {C11.instance_variable_get('@result1')} == false
2644
+ ok {C11.instance_variable_get('@result2')} == true
2645
+ end
2646
+
2647
+ end
2648
+
2649
+
2650
+ topic '.put()' do
2651
+
2652
+ spec "[!h9b47] defines getter method." do
2653
+ class C21 < K8::BaseConfig
2654
+ put :hom, 123, "HomHom"
2655
+ end
2656
+ ok {C21.instance_methods}.include?(:hom)
2657
+ ok {C21.new.hom} == 123
2658
+ end
2659
+
2660
+ spec "[!ncwzt] stores key with value, description and secret flag." do
2661
+ class C22 < K8::BaseConfig
2662
+ put :hom, 123, "HomHom"
2663
+ put :hom2, SECRET, "Secret HomHom"
2664
+ end
2665
+ ok {C22.instance_variable_get('@__all')} == {
2666
+ :hom => [123, "HomHom", false],
2667
+ :hom2 => [K8::BaseConfig::SECRET, "Secret HomHom", true],
2668
+ }
2669
+ end
2670
+
2671
+ spec "[!mun1v] keeps secret flag." do
2672
+ class C23 < K8::BaseConfig
2673
+ put :haruhi , 'C' , "Suzumiya"
2674
+ put :mikuru , SECRET, "Asahina"
2675
+ put :yuki , SECRET, "Nagato"
2676
+ end
2677
+ class C23
2678
+ put :mikuru , 'F'
2679
+ end
2680
+ ok {C23.instance_variable_get('@__all')} == {
2681
+ :haruhi => ['C', "Suzumiya", false],
2682
+ :mikuru => ['F', "Asahina", true],
2683
+ :yuki => [K8::BaseConfig::SECRET, "Nagato", true],
2684
+ }
2685
+ end
2686
+
2687
+ end
2688
+
2689
+
2690
+ topic '.add()' do
2691
+
2692
+ spec "[!envke] raises error when already added." do
2693
+ class C31 < K8::BaseConfig
2694
+ add :hom, 123, "HomHom"
2695
+ @ex = nil
2696
+ begin
2697
+ add :hom, 456, "HomHom"
2698
+ rescue => ex
2699
+ @ex = ex
2700
+ end
2701
+ end
2702
+ ex = C31.instance_variable_get('@ex')
2703
+ ok {ex} != nil
2704
+ ok {ex}.is_a?(K8::ConfigError)
2705
+ ok {ex.message} == "add(:hom, 456): cannot add because already added; use set() or put() instead."
2706
+ end
2707
+
2708
+ spec "[!6cmb4] adds new key, value and desc." do
2709
+ class C32 < K8::BaseConfig
2710
+ add :hom, 123, "HomHom"
2711
+ add :hom2, 'HOM'
2712
+ end
2713
+ all = C32.instance_variable_get('@__all')
2714
+ ok {all} == {:hom=>[123, "HomHom", false], :hom2=>['HOM', nil, false]}
2715
+ end
2716
+
2717
+ end
2718
+
2719
+
2720
+ topic '.set()' do
2721
+
2722
+ spec "[!2yis0] raises error when not added yet." do
2723
+ class C41 < K8::BaseConfig
2724
+ @ex = nil
2725
+ begin
2726
+ set :hom, 123, "HomHom"
2727
+ rescue => ex
2728
+ @ex = ex
2729
+ end
2730
+ end
2731
+ ex = C41.instance_variable_get('@ex')
2732
+ ok {ex} != nil
2733
+ ok {ex}.is_a?(K8::ConfigError)
2734
+ ok {ex.message} == "add(:hom, 123): cannot set because not added yet; use add() or put() instead."
2735
+ end
2736
+
2737
+ spec "[!3060g] sets key, value and desc." do
2738
+ class C42 < K8::BaseConfig
2739
+ add :hom, 123, "HomHom"
2740
+ end
2741
+ class C42
2742
+ set :hom, 456
2743
+ end
2744
+ all = C42.instance_variable_get('@__all')
2745
+ ok {all} == {:hom=>[456, "HomHom", false]}
2746
+ end
2747
+
2748
+ end
2749
+
2750
+
2751
+ topic '.each()' do
2752
+
2753
+ spec "[!iu88i] yields with key, value, desc and secret flag." do
2754
+ class C51 < K8::BaseConfig
2755
+ add :haruhi , 'C' , "Suzumiya"
2756
+ add :mikuru , SECRET, "Asahina"
2757
+ add :yuki , 'A' , "Nagato"
2758
+ end
2759
+ class C51
2760
+ set :mikuru , 'F'
2761
+ add :sasaki , 'B'
2762
+ end
2763
+ #
2764
+ arr = []
2765
+ C51.each {|*args| arr << args }
2766
+ ok {arr} == [
2767
+ [:haruhi, 'C', "Suzumiya", false],
2768
+ [:mikuru, 'F', "Asahina", true],
2769
+ [:yuki, 'A', "Nagato", false],
2770
+ [:sasaki, 'B', nil, false],
2771
+ ]
2772
+ end
2773
+
2774
+ end
2775
+
2776
+
2777
+ topic '.get()' do
2778
+
2779
+ spec "[!zlhnp] returns value corresponding to key." do
2780
+ class C61 < K8::BaseConfig
2781
+ add :haruhi , 'C' , "Suzumiya"
2782
+ add :mikuru , 'E' , "Asahina"
2783
+ add :yuki , 'A' , "Nagato"
2784
+ end
2785
+ class C61
2786
+ set :mikuru , 'F'
2787
+ add :sasaki , 'B'
2788
+ end
2789
+ ok {C61.get(:haruhi)} == 'C'
2790
+ ok {C61.get(:mikuru)} == 'F'
2791
+ ok {C61.get(:yuki)} == 'A'
2792
+ ok {C61.get(:sasaki)} == 'B'
2793
+ end
2794
+
2795
+ spec "[!o0k05] returns default value (=nil) when key is not added." do
2796
+ class C62 < K8::BaseConfig
2797
+ add :haruhi , 'C' , "Suzumiya"
2798
+ add :mikuru , 'E' , "Asahina"
2799
+ add :yuki , 'A' , "Nagato"
2800
+ end
2801
+ ok {C62.get(:sasaki)} == nil
2802
+ ok {C62.get(:sasaki, "")} == ""
2803
+ end
2804
+
2805
+ end
2806
+
2807
+
2808
+ topic '[](key)' do
2809
+
2810
+ spec "[!jn9l5] returns value corresponding to key." do
2811
+ class C71 < K8::BaseConfig
2812
+ add :haruhi , 'C' , "Suzumiya"
2813
+ add :mikuru , 'E' , "Asahina"
2814
+ add :yuki , 'A' , "Nagato"
2815
+ end
2816
+ class C71
2817
+ set :mikuru , 'F'
2818
+ add :sasaki , 'B'
2819
+ end
2820
+ c = C71.new
2821
+ ok {c[:haruhi]} == 'C'
2822
+ ok {c[:mikuru]} == 'F'
2823
+ ok {c[:yuki]} == 'A'
2824
+ ok {c[:sasaki]} == 'B'
2825
+ end
2826
+
2827
+ end
2828
+
2829
+
2830
+ topic '#get_all()' do
2831
+
2832
+ spec "[!4ik3c] returns all keys and values which keys start with prefix as hash object." do
2833
+ class C81 < K8::BaseConfig
2834
+ add :session_cookie_name , 'rack.sess'
2835
+ add :session_cookie_expires , 30*60*60
2836
+ add :session_cookie_secure , true
2837
+ add :name , 'Homhom'
2838
+ add :secure , false
2839
+ end
2840
+ #
2841
+ c = C81.new
2842
+ ok {c.get_all(:session_cookie_)} == {
2843
+ :name => 'rack.sess',
2844
+ :expires => 30*60*60,
2845
+ :secure => true,
2846
+ }
2847
+ end
2848
+
2849
+ end
2850
+
2851
+
2852
+ end
2853
+
2854
+
2855
+ topic K8::Mock do
2856
+
2857
+
2858
+ topic '.new_env()' do
2859
+
2860
+ spec "[!c779l] raises ArgumentError when both form and json are specified." do
2861
+ pr = proc { K8::Mock.new_env(form: "x=1", json: {"y"=>2}) }
2862
+ ok {pr}.raise?(ArgumentError, "new_env(): not allowed both 'form' and 'json' at a time.")
2863
+ end
2864
+
2865
+ spec "[!gko8g] 'multipart:' kwarg accepts Hash object (which is converted into multipart data)." do
2866
+ env = K8::Mock.new_env(multipart: {"a"=>10, "b"=>20})
2867
+ ok {env['CONTENT_TYPE']} =~ /\Amultipart\/form-data; *boundary=/
2868
+ env['CONTENT_TYPE'] =~ /\Amultipart\/form-data; *boundary=(.+)/
2869
+ boundary = $1
2870
+ cont_len = Integer(env['CONTENT_LENGTH'])
2871
+ params, files = K8::Util.parse_multipart(env['rack.input'], boundary, cont_len)
2872
+ ok {params} == {"a"=>"10", "b"=>"20"}
2873
+ ok {files} == {}
2874
+ end
2875
+
2876
+ end
2877
+
2878
+
2879
+ end
2880
+
2881
+
2882
+ topic K8::Mock::MultipartBuilder do
2883
+
2884
+
2885
+ topic '#initialize()' do
2886
+
2887
+ spec "[!ajfgl] sets random string as boundary when boundary is nil." do
2888
+ arr = []
2889
+ 1000.times do
2890
+ mp = K8::Mock::MultipartBuilder.new(nil)
2891
+ ok {mp.boundary} != nil
2892
+ ok {mp.boundary}.is_a?(String)
2893
+ arr << mp.boundary
2894
+ end
2895
+ ok {arr.sort.uniq.length} == 1000
2896
+ end
2897
+
2898
+ end
2899
+
2900
+
2901
+ topic '#add()' do
2902
+
2903
+ spec "[!tp4bk] detects content type from filename when filename is not nil." do
2904
+ mp = K8::Mock::MultipartBuilder.new
2905
+ mp.add("name1", "value1")
2906
+ mp.add("name2", "value2", "foo.csv")
2907
+ mp.add("name3", "value3", "bar.csv", "text/plain")
2908
+ ok {mp.instance_variable_get('@params')} == [
2909
+ ["name1", "value1", nil, nil],
2910
+ ["name2", "value2", "foo.csv", "text/comma-separated-values"],
2911
+ ["name3", "value3", "bar.csv", "text/plain"],
2912
+ ]
2913
+ end
2914
+
2915
+ end
2916
+
2917
+
2918
+ topic '#add_file()' do
2919
+
2920
+ fixture :data_dir do
2921
+ File.join(File.dirname(__FILE__), 'data')
2922
+ end
2923
+
2924
+ fixture :filename1 do |data_dir|
2925
+ File.join(data_dir, 'example1.png')
2926
+ end
2927
+
2928
+ fixture :filename2 do |data_dir|
2929
+ File.join(data_dir, 'example1.jpg')
2930
+ end
2931
+
2932
+ fixture :multipart_data do |data_dir|
2933
+ fname = File.join(data_dir, 'multipart.form')
2934
+ File.open(fname, 'rb') {|f| f.read }
2935
+ end
2936
+
2937
+
2938
+ spec "[!uafqa] detects content type from filename when content type is not provided." do
2939
+ |filename1, filename2|
2940
+ file1 = File.open(filename1)
2941
+ file2 = File.open(filename2)
2942
+ at_end { [file1, file2].each {|f| f.close() unless f.closed? } }
2943
+ mp = K8::Mock::MultipartBuilder.new
2944
+ mp.add_file('image1', file1)
2945
+ mp.add_file('image2', file2)
2946
+ mp.instance_exec(self) do |_|
2947
+ _.ok {@params[0][2]} == "example1.png"
2948
+ _.ok {@params[0][3]} == "image/png"
2949
+ _.ok {@params[1][2]} == "example1.jpg"
2950
+ _.ok {@params[1][3]} == "image/jpeg"
2951
+ end
2952
+ end
2953
+
2954
+ spec "[!b5811] reads file content and adds it as param value." do
2955
+ |filename1, filename2, multipart_data|
2956
+ file1 = File.open(filename1)
2957
+ file2 = File.open(filename2)
2958
+ at_end { [file1, file2].each {|f| f.close() unless f.closed? } }
2959
+ boundary = '---------------------------68927884511827559971471404947'
2960
+ mp = K8::Mock::MultipartBuilder.new(boundary)
2961
+ mp.add('text1', "test1")
2962
+ mp.add('text2', "日本語\r\nあいうえお\r\n")
2963
+ mp.add_file('file1', file1)
2964
+ mp.add_file('file2', file2)
2965
+ ok {mp.to_s} == multipart_data
2966
+ end
2967
+
2968
+ spec "[!36bsu] closes opened file automatically." do
2969
+ |filename1, filename2, multipart_data|
2970
+ file1 = File.open(filename1)
2971
+ file2 = File.open(filename2)
2972
+ at_end { [file1, file2].each {|f| f.close() unless f.closed? } }
2973
+ ok {file1.closed?} == false
2974
+ ok {file2.closed?} == false
2975
+ mp = K8::Mock::MultipartBuilder.new()
2976
+ mp.add_file('file1', file1)
2977
+ mp.add_file('file2', file2)
2978
+ ok {file1.closed?} == true
2979
+ ok {file2.closed?} == true
2980
+ end
2981
+
2982
+ end
2983
+
2984
+
2985
+ topic '#to_s()' do
2986
+
2987
+ spec "[!61gc4] returns multipart form string." do
2988
+ mp = K8::Mock::MultipartBuilder.new("abc123")
2989
+ mp.add("name1", "value1")
2990
+ mp.add("name2", "value2", "foo.txt", "text/plain")
2991
+ s = mp.to_s
2992
+ ok {s} == [
2993
+ "--abc123\r\n",
2994
+ "Content-Disposition: form-data; name=\"name1\"\r\n",
2995
+ "\r\n",
2996
+ "value1\r\n",
2997
+ "--abc123\r\n",
2998
+ "Content-Disposition: form-data; name=\"name2\"; filename=\"foo.txt\"\r\n",
2999
+ "Content-Type: text/plain\r\n",
3000
+ "\r\n",
3001
+ "value2\r\n",
3002
+ "--abc123--\r\n",
3003
+ ].join()
3004
+ #
3005
+ params, files = K8::Util.parse_multipart(StringIO.new(s), "abc123", s.length)
3006
+ begin
3007
+ ok {params} == {'name1'=>"value1", 'name2'=>"foo.txt"}
3008
+ ok {files.keys} == ['name2']
3009
+ ok {files['name2'].filename} == "foo.txt"
3010
+ ensure
3011
+ fpath = files['name2'].tmp_filepath
3012
+ File.unlink(fpath) if File.exist?(fpath)
3013
+ end
3014
+ end
3015
+
3016
+ end
3017
+
3018
+
3019
+ end
3020
+
3021
+
3022
+ topic K8::Mock::TestApp do
3023
+
3024
+
3025
+ topic '#request()' do
3026
+
3027
+ spec "[!4xpwa] creates env object and calls app with it." do
3028
+ rackapp = proc {|env|
3029
+ body = [
3030
+ "PATH_INFO: #{env['PATH_INFO']}\n",
3031
+ "QUERY_STRING: #{env['QUERY_STRING']}\n",
3032
+ "HTTP_COOKIE: #{env['HTTP_COOKIE']}\n",
3033
+ ]
3034
+ [200, {"Content-Type"=>"text/plain"}, body]
3035
+ }
3036
+ http = K8::Mock::TestApp.new(rackapp)
3037
+ resp = http.GET('/foo', query: {"x"=>123}, cookie: {"k"=>"v"})
3038
+ ok {resp.status} == 200
3039
+ ok {resp.headers} == {"Content-Type"=>"text/plain"}
3040
+ ok {resp.body} == [
3041
+ "PATH_INFO: /foo\n",
3042
+ "QUERY_STRING: x=123\n",
3043
+ "HTTP_COOKIE: k=v\n",
3044
+ ]
3045
+ end
3046
+
3047
+ end
3048
+
3049
+ end
3050
+
3051
+
3052
+ topic K8::Mock::TestResponse do
3053
+
3054
+
3055
+ topic '#body_binary' do
3056
+
3057
+ spec "[!mb0i4] returns body as binary string." do
3058
+ resp = K8::Mock::TestResponse.new(200, {}, ["foo", "bar"])
3059
+ ok {resp.body_binary} == "foobar"
3060
+ #ok {resp.body_binary.encoding} == Encoding::UTF_8
3061
+ end
3062
+
3063
+ end
3064
+
3065
+
3066
+ topic '#body_text' do
3067
+
3068
+ spec "[!rr18d] error when 'Content-Type' header is missing." do
3069
+ resp = K8::Mock::TestResponse.new(200, {}, ["foo", "bar"])
3070
+ pr = proc { resp.body_text }
3071
+ ok {pr}.raise?(RuntimeError, "body_text(): missing 'Content-Type' header.")
3072
+ end
3073
+
3074
+ spec "[!dou1n] converts body text according to 'charset' in 'Content-Type' header." do
3075
+ ctype = "application/json;charset=us-ascii"
3076
+ resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3077
+ ok {resp.body_text} == '{"a":123}'
3078
+ ok {resp.body_text.encoding} == Encoding::ASCII
3079
+ end
3080
+
3081
+ spec "[!cxje7] assumes charset as 'utf-8' when 'Content-Type' is json." do
3082
+ ctype = "application/json"
3083
+ resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3084
+ ok {resp.body_text} == '{"a":123}'
3085
+ ok {resp.body_text.encoding} == Encoding::UTF_8
3086
+ end
3087
+
3088
+ spec "[!n4c71] error when non-json 'Content-Type' header has no 'charset'." do
3089
+ ctype = "text/plain"
3090
+ resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ["foo", "bar"])
3091
+ pr = proc { resp.body_text }
3092
+ ok {pr}.raise?(RuntimeError, "body_text(): missing 'charset' in 'Content-Type' header.")
3093
+ end
3094
+
3095
+ spec "[!vkj9h] returns body as text string, according to 'charset' in 'Content-Type'." do
3096
+ ctype = "text/plain;charset=utf-8"
3097
+ resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ["foo", "bar"])
3098
+ ok {resp.body_text} == "foobar"
3099
+ ok {resp.body_text.encoding} == Encoding::UTF_8
3100
+ end
3101
+
3102
+ end
3103
+
3104
+
3105
+ topic '#body_json' do
3106
+
3107
+ spec "[!qnic1] returns Hash object representing JSON string." do
3108
+ ctype = "application/json"
3109
+ resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3110
+ ok {resp.body_json} == {"a"=>123}
3111
+ end
3112
+
3113
+ end
3114
+
3115
+
3116
+ topic '#content_type' do
3117
+
3118
+ spec "[!40hcz] returns 'Content-Type' header value." do
3119
+ ctype = "application/json"
3120
+ resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3121
+ ok {resp.content_type} == ctype
3122
+ end
3123
+
3124
+ end
3125
+
3126
+
3127
+ topic '#content_length' do
3128
+
3129
+ spec "[!5lb19] returns 'Content-Length' header value as integer." do
3130
+ resp = K8::Mock::TestResponse.new(200, {'Content-Length'=>"0"}, [])
3131
+ ok {resp.content_length} == 0
3132
+ ok {resp.content_length}.is_a?(Fixnum)
3133
+ end
3134
+
3135
+ spec "[!qjktz] returns nil when 'Content-Length' is not set." do
3136
+ resp = K8::Mock::TestResponse.new(200, {}, [])
3137
+ ok {resp.content_length} == nil
3138
+ end
3139
+
3140
+ end
3141
+
3142
+
3143
+ topic '#location' do
3144
+
3145
+ spec "[!8y8lg] returns 'Location' header value." do
3146
+ resp = K8::Mock::TestResponse.new(200, {'Location'=>'/top'}, [])
3147
+ ok {resp.location} == "/top"
3148
+ end
3149
+
3150
+ end
3151
+
3152
+
3153
+ end
3154
+
3155
+
3156
+ end
3157
+
3158
+
3159
+ if __FILE__ == $0
3160
+ Oktest::main()
3161
+ end