keight 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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