runo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. data/LICENSE +19 -0
  2. data/README.rdoc +120 -0
  3. data/bin/runo +35 -0
  4. data/lib/_error.rb +14 -0
  5. data/lib/_field.rb +260 -0
  6. data/lib/_i18n.rb +141 -0
  7. data/lib/_parser.rb +243 -0
  8. data/lib/_path.rb +86 -0
  9. data/lib/_storage/_storage.rb +213 -0
  10. data/lib/_storage/file.rb +200 -0
  11. data/lib/_storage/sequel.rb +174 -0
  12. data/lib/_storage/temp.rb +73 -0
  13. data/lib/_widget/action_create.rb +23 -0
  14. data/lib/_widget/action_login.rb +22 -0
  15. data/lib/_widget/action_signup.rb +16 -0
  16. data/lib/_widget/action_update.rb +16 -0
  17. data/lib/_widget/crumb.rb +24 -0
  18. data/lib/_widget/done.rb +16 -0
  19. data/lib/_widget/login.rb +25 -0
  20. data/lib/_widget/me.rb +31 -0
  21. data/lib/_widget/message.rb +51 -0
  22. data/lib/_widget/navi.rb +88 -0
  23. data/lib/_widget/submit.rb +49 -0
  24. data/lib/_widget/view_ym.rb +77 -0
  25. data/lib/_workflow/_workflow.rb +89 -0
  26. data/lib/_workflow/attachment.rb +50 -0
  27. data/lib/_workflow/blog.rb +28 -0
  28. data/lib/_workflow/contact.rb +23 -0
  29. data/lib/_workflow/forum.rb +26 -0
  30. data/lib/_workflow/register.rb +39 -0
  31. data/lib/meta/_meta.rb +20 -0
  32. data/lib/meta/group.rb +19 -0
  33. data/lib/meta/id.rb +59 -0
  34. data/lib/meta/owner.rb +21 -0
  35. data/lib/meta/timestamp.rb +118 -0
  36. data/lib/runo.rb +396 -0
  37. data/lib/scalar/checkbox.rb +68 -0
  38. data/lib/scalar/file.rb +144 -0
  39. data/lib/scalar/img.rb +112 -0
  40. data/lib/scalar/password.rb +58 -0
  41. data/lib/scalar/radio.rb +47 -0
  42. data/lib/scalar/select.rb +47 -0
  43. data/lib/scalar/text.rb +38 -0
  44. data/lib/scalar/textarea.rb +35 -0
  45. data/lib/scalar/textarea_pre.rb +14 -0
  46. data/lib/scalar/textarea_wiki.rb +173 -0
  47. data/lib/set/_set.rb +195 -0
  48. data/lib/set/dynamic.rb +177 -0
  49. data/lib/set/static.rb +102 -0
  50. data/lib/set/static_folder.rb +96 -0
  51. data/locale/en/index.po +242 -0
  52. data/locale/index.pot +243 -0
  53. data/locale/ja/index.po +242 -0
  54. data/locale/lazy_parser.rb +54 -0
  55. data/skel/config.ru +27 -0
  56. data/skel/skin/_users/00000000_frank-avatar.jpg +0 -0
  57. data/skel/skin/_users/00000000_frank-avatar_small.jpg +0 -0
  58. data/skel/skin/_users/00000000_frank.yaml +12 -0
  59. data/skel/skin/_users/00000000_root-avatar.jpg +0 -0
  60. data/skel/skin/_users/00000000_root-avatar_small.jpg +0 -0
  61. data/skel/skin/_users/00000000_root.yaml +11 -0
  62. data/skel/skin/_users/css/users.css +21 -0
  63. data/skel/skin/_users/css/users.less +25 -0
  64. data/skel/skin/_users/done.html +42 -0
  65. data/skel/skin/_users/index.html +46 -0
  66. data/skel/skin/_users/index.yaml +3 -0
  67. data/skel/skin/_users/summary.html +40 -0
  68. data/skel/skin/css/base.css +93 -0
  69. data/skel/skin/css/base.less +139 -0
  70. data/skel/skin/css/coax.css +199 -0
  71. data/skel/skin/css/coax.less +244 -0
  72. data/skel/skin/examples/blog/20091214_0001.yaml +8 -0
  73. data/skel/skin/examples/blog/20100630_0001.yaml +8 -0
  74. data/skel/skin/examples/blog/20100630_0002.yaml +14 -0
  75. data/skel/skin/examples/blog/20100701_0001.yaml +8 -0
  76. data/skel/skin/examples/blog/20100701_0002-a-20100701_0001-f.jpg +0 -0
  77. data/skel/skin/examples/blog/20100701_0002-a-20100701_0001-f_small.jpg +0 -0
  78. data/skel/skin/examples/blog/20100701_0002.yaml +19 -0
  79. data/skel/skin/examples/blog/frank/20100701_0001.yaml +10 -0
  80. data/skel/skin/examples/blog/frank/index.yaml +4 -0
  81. data/skel/skin/examples/blog/index.html +51 -0
  82. data/skel/skin/examples/blog/rss.xml +18 -0
  83. data/skel/skin/examples/contact/20100701_0001-file.txt +1 -0
  84. data/skel/skin/examples/contact/20100701_0001.yaml +15 -0
  85. data/skel/skin/examples/contact/20100701_0002.yaml +8 -0
  86. data/skel/skin/examples/contact/20100701_0003.yaml +9 -0
  87. data/skel/skin/examples/contact/index.html +47 -0
  88. data/skel/skin/examples/contact/js/contact.js +13 -0
  89. data/skel/skin/examples/contact/summary.html +54 -0
  90. data/skel/skin/examples/forum/20100701_0001.yaml +41 -0
  91. data/skel/skin/examples/forum/20100701_0002.yaml +25 -0
  92. data/skel/skin/examples/forum/index.html +68 -0
  93. data/skel/skin/examples/forum/summary.html +47 -0
  94. data/skel/skin/examples/index.html +75 -0
  95. data/skel/skin/index.html +41 -0
  96. data/skel/skin/js/base.js +50 -0
  97. data/t/locale/de/index.po +19 -0
  98. data/t/locale/en-GB/index.po +25 -0
  99. data/t/locale/ja/index.po +30 -0
  100. data/t/skin/_users/00000000_test.yaml +3 -0
  101. data/t/skin/_users/index.html +13 -0
  102. data/t/skin/foo/20091120_0001.yaml +7 -0
  103. data/t/skin/foo/bar/20091120_0001.yaml +5 -0
  104. data/t/skin/foo/bar/index.yaml +5 -0
  105. data/t/skin/foo/baz/css/baz.css +1 -0
  106. data/t/skin/foo/css/foo.css +1 -0
  107. data/t/skin/foo/index.html +14 -0
  108. data/t/skin/foo/index.yaml +7 -0
  109. data/t/skin/foo/not_css/foo.css +1 -0
  110. data/t/skin/foo/sub-20100306_0001.yaml +3 -0
  111. data/t/skin/index.yaml +3 -0
  112. data/t/skin/t_attachment/index.html +13 -0
  113. data/t/skin/t_contact/done.html +6 -0
  114. data/t/skin/t_contact/index.html +9 -0
  115. data/t/skin/t_file/index.html +16 -0
  116. data/t/skin/t_img/index.html +14 -0
  117. data/t/skin/t_img/test.jpg +0 -0
  118. data/t/skin/t_select/index.html +9 -0
  119. data/t/skin/t_store/index.html +9 -0
  120. data/t/skin/t_summary/20100326_0001.yaml +3 -0
  121. data/t/skin/t_summary/create.html +9 -0
  122. data/t/skin/t_summary/index.html +9 -0
  123. data/t/skin/t_summary/summary.html +9 -0
  124. data/t/t.rb +27 -0
  125. data/t/test_checkbox.rb +273 -0
  126. data/t/test_field.rb +330 -0
  127. data/t/test_file.rb +900 -0
  128. data/t/test_id.rb +215 -0
  129. data/t/test_img.rb +328 -0
  130. data/t/test_meta.rb +57 -0
  131. data/t/test_parser.rb +1266 -0
  132. data/t/test_password.rb +188 -0
  133. data/t/test_radio.rb +226 -0
  134. data/t/test_role.rb +249 -0
  135. data/t/test_runo.rb +742 -0
  136. data/t/test_runo_call.rb +1286 -0
  137. data/t/test_runo_i18n.rb +318 -0
  138. data/t/test_select.rb +182 -0
  139. data/t/test_set_complex.rb +527 -0
  140. data/t/test_set_dynamic.rb +1504 -0
  141. data/t/test_set_folder.rb +515 -0
  142. data/t/test_set_permit.rb +246 -0
  143. data/t/test_set_static.rb +445 -0
  144. data/t/test_storage.rb +915 -0
  145. data/t/test_text.rb +125 -0
  146. data/t/test_textarea.rb +138 -0
  147. data/t/test_timestamp.rb +473 -0
  148. data/t/test_workflow.rb +367 -0
  149. metadata +345 -0
@@ -0,0 +1,900 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ require "#{::File.dirname __FILE__}/t"
7
+
8
+ class TC_File < Test::Unit::TestCase
9
+
10
+ def setup
11
+ Runo.current[:uri] = nil
12
+
13
+ @file = Class.new
14
+ @file.stubs(:rewind).returns(nil)
15
+ @file.stubs(:read).returns('this is file body')
16
+ @file.stubs(:length).returns(@file.read.length)
17
+
18
+ meta = nil
19
+ Runo::Parser.gsub_scalar('$(foo file 1..50 jpg, gif, png)') {|id, m|
20
+ meta = m
21
+ ''
22
+ }
23
+ @f = Runo::Field.instance meta.merge(:id => 'foo')
24
+ end
25
+
26
+ def test_meta
27
+ assert_equal(
28
+ 1,
29
+ @f[:min],
30
+ 'File#initialize should set :min from the range token'
31
+ )
32
+ assert_equal(
33
+ 50,
34
+ @f[:max],
35
+ 'File#initialize should set :max from the range token'
36
+ )
37
+ assert_equal(
38
+ ['jpg', 'gif', 'png'],
39
+ @f[:options],
40
+ 'File#initialize should set :options from the csv token'
41
+ )
42
+ end
43
+
44
+ def test_meta_options
45
+ meta = nil
46
+ Runo::Parser.gsub_scalar('$(foo file jpg, GIF, Png)') {|id, m|
47
+ meta = m
48
+ ''
49
+ }
50
+ @f = Runo::Field.instance meta.merge(:id => 'foo')
51
+ assert_equal(
52
+ ['jpg', 'gif', 'png'],
53
+ @f[:options],
54
+ 'File#initialize should downcase :options'
55
+ )
56
+ end
57
+
58
+ def test_meta_path
59
+ @f[:parent] = Runo::Set::Static::Folder.root.item('t_file', 'main')
60
+ assert_equal(
61
+ '/t_file/main/foo',
62
+ @f[:path],
63
+ 'File#meta_path should return the full path to the field'
64
+ )
65
+ end
66
+
67
+ def test_meta_tmp_path
68
+ @f[:parent] = Runo::Set::Static::Folder.root.item('t_file', 'main')
69
+ Runo.current[:base] = @f[:parent]
70
+ tid = @f[:parent][:tid]
71
+
72
+ assert_equal(
73
+ "/t_file/#{tid}/foo",
74
+ @f[:tmp_path],
75
+ 'File#meta_tmp_path should return the short path from the tid'
76
+ )
77
+
78
+ Runo.current[:base] = nil
79
+ assert_nil(
80
+ @f[:tmp_path],
81
+ 'File#meta_tmp_path should return nil unless Runo.base is set'
82
+ )
83
+ end
84
+
85
+ def test_meta_persistent_sd
86
+ root = Runo::Set::Static::Folder.root.item('t_file', 'main')
87
+ parent = Runo::Set::Dynamic.new(:id => 'boo', :parent => root)
88
+ @f[:parent] = parent
89
+ assert_equal(
90
+ root,
91
+ @f[:persistent_sd],
92
+ 'File#persistent_sd should return the nearest persient sd'
93
+ )
94
+ end
95
+
96
+ def test_meta_persistent_name
97
+ root = Runo::Set::Static::Folder.root.item('t_file', 'main')
98
+ parent = Runo::Set::Dynamic.new(:id => 'boo', :parent => root)
99
+ @f[:parent] = parent
100
+ assert_equal(
101
+ 'boo-foo',
102
+ @f[:persistent_name],
103
+ 'File#persistent_name should return the path to [:persistent_sd]'
104
+ )
105
+ end
106
+
107
+ def test_val_cast_from_rack
108
+ @f.create(
109
+ :type => 'image/jpeg',
110
+ :tempfile => @file,
111
+ :head => <<'_eos',
112
+ Content-Disposition: form-data; name="t_file"; filename="baz.jpg"
113
+ Content-Type: image/jpeg
114
+ _eos
115
+ :filename => 'baz.jpg',
116
+ :name => 't_file'
117
+ )
118
+
119
+ assert_equal(
120
+ {
121
+ 'basename' => 'baz.jpg',
122
+ 'type' => 'image/jpeg',
123
+ 'size' => @file.length,
124
+ },
125
+ @f.val,
126
+ 'File#val_cast should re-map a hash from Rack'
127
+ )
128
+ assert_equal(
129
+ @file.read,
130
+ @f.body,
131
+ 'File#val_cast should store the file body in @body'
132
+ )
133
+ end
134
+
135
+ def test_val_cast_load
136
+ @f.load(
137
+ 'basename' => 'baz.jpg',
138
+ 'type' => 'image/jpeg',
139
+ 'size' => 123
140
+ )
141
+ assert_equal(
142
+ {
143
+ 'basename' => 'baz.jpg',
144
+ 'type' => 'image/jpeg',
145
+ 'size' => 123,
146
+ },
147
+ @f.val,
148
+ 'File#val_cast should load() a hash without :tempfile like Set#load'
149
+ )
150
+
151
+ @f.load(
152
+ {}
153
+ )
154
+ assert_equal(
155
+ {},
156
+ @f.val,
157
+ 'File#val_cast should load() a hash without :tempfile like Set#load'
158
+ )
159
+ end
160
+
161
+ def test_get
162
+ @f[:parent] = Runo::Set::Static::Folder.root.item('t_file', 'main')
163
+ Runo.current[:base] = @f[:parent]
164
+ tid = @f[:parent][:tid]
165
+
166
+ @f.load({})
167
+ assert_nil(
168
+ @f.get,
169
+ 'File#get should return nil when the val is empty'
170
+ )
171
+
172
+ @f.load(
173
+ 'basename' => 'baz.jpg',
174
+ 'type' => 'image/jpeg',
175
+ 'size' => 12
176
+ )
177
+ assert_equal(
178
+ '<a href="/t_file/main/foo/baz.jpg">baz.jpg (12 bytes)</a>',
179
+ @f.get,
180
+ 'File#get should return proper string'
181
+ )
182
+ assert_equal(
183
+ <<"_html",
184
+ <span class="file">
185
+ <a href="/t_file/#{tid}/foo/baz.jpg">baz.jpg (12 bytes)</a>
186
+ <input type="file" name="foo" size="" class="file" />
187
+ </span>
188
+ _html
189
+ @f.get(:action => :update),
190
+ 'File#get should return proper string'
191
+ )
192
+
193
+ @f.load(
194
+ 'basename' => '<baz>.jpg',
195
+ 'type' => 'image/<jpeg>',
196
+ 'size' => 12
197
+ )
198
+ assert_equal(
199
+ '<a href="/t_file/main/foo/&lt;baz&gt;.jpg">&lt;baz&gt;.jpg (12 bytes)</a>',
200
+ @f.get,
201
+ 'File#get should escape the special characters in file information'
202
+ )
203
+ end
204
+
205
+ def test_get_hidden
206
+ Runo.client = 'root'
207
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
208
+ sd.update(
209
+ '_1' => {
210
+ 'baz' => {
211
+ '_1' => {},
212
+ },
213
+ }
214
+ )
215
+ Runo.current[:base] = sd
216
+ sd[:tid] = '1234.567'
217
+
218
+ assert_equal(
219
+ <<'_html',
220
+ <span class="file">
221
+
222
+ <input type="file" name="_1-foo" size="" class="file" />
223
+ </span>
224
+ _html
225
+ sd.item('_1', 'foo').get(:action => :create),
226
+ 'File#get should not include a hidden input if the field is not required'
227
+ )
228
+ assert_equal(
229
+ <<'_html',
230
+ <span class="file">
231
+
232
+ <input type="hidden" name="_1-baz-_1-qux" value="" />
233
+ <input type="file" name="_1-baz-_1-qux" size="" class="file" />
234
+ </span>
235
+ _html
236
+ sd.item('_1', 'baz', '_1', 'qux').get(:action => :create),
237
+ 'File#get should include a hidden input to supplement an empty field'
238
+ )
239
+
240
+ sd.update(
241
+ '_1' => {
242
+ 'baz' => {
243
+ '_1' => {
244
+ 'qux' => {
245
+ :type => 'image/jpeg',
246
+ :tempfile => @file,
247
+ :head => <<'_eos',
248
+ Content-Disposition: form-data; name="t_file"; filename="qux.jpg"
249
+ Content-Type: image/jpeg
250
+ _eos
251
+ :filename => 'qux.jpg',
252
+ :name => 't_file'
253
+ },
254
+ },
255
+ },
256
+ }
257
+ )
258
+ assert_equal(
259
+ <<"_html",
260
+ <span class="file">
261
+ <a href="/t_file/1234.567/_1/baz/_1/qux/qux.jpg">qux.jpg (#{@file.length} bytes)</a>
262
+ <input type="file" name="_1-baz-_1-qux" size="" class="file" />
263
+ </span>
264
+ _html
265
+ sd.item('_1', 'baz', '_1', 'qux').get(:action => :update),
266
+ 'File#get should not include a hidden if the field is not empty'
267
+ )
268
+ end
269
+
270
+ def test_get_delete
271
+ Runo.client = 'root'
272
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
273
+ sd.update(
274
+ '_1' => {
275
+ 'baz' => {
276
+ '_1' => {},
277
+ },
278
+ }
279
+ )
280
+ Runo.current[:base] = sd
281
+ sd[:tid] = '1234.567'
282
+
283
+ assert_equal(
284
+ <<'_html',
285
+ <span class="file">
286
+
287
+ <input type="file" name="_1-foo" size="" class="file" />
288
+ </span>
289
+ _html
290
+ sd.item('_1', 'foo').get(:action => :create),
291
+ 'File#get should not include a delete submit if the field is empty'
292
+ )
293
+
294
+ sd.update(
295
+ '_1' => {
296
+ 'foo' => {
297
+ :type => 'image/jpeg',
298
+ :tempfile => @file,
299
+ :head => <<'_eos',
300
+ Content-Disposition: form-data; name="t_file"; filename="foo.jpg"
301
+ Content-Type: image/jpeg
302
+ _eos
303
+ :filename => 'foo.jpg',
304
+ :name => 't_file'
305
+ },
306
+ }
307
+ )
308
+ assert_equal(
309
+ <<"_html",
310
+ <span class="file">
311
+ <a href="/t_file/1234.567/_1/foo/foo.jpg">foo.jpg (#{@file.length} bytes)</a>
312
+ <input type="file" name="_1-foo" size="" class="file" />
313
+ <input type="submit" name="_1-foo.action-delete" value="delete" />
314
+ </span>
315
+ _html
316
+ sd.item('_1', 'foo').get(:action => :update),
317
+ 'File#get should include a delete submit if the field is not empty'
318
+ )
319
+
320
+ sd.item('_1', 'foo')[:min] = 1
321
+ assert_equal(
322
+ <<"_html",
323
+ <span class="file">
324
+ <a href="/t_file/1234.567/_1/foo/foo.jpg">foo.jpg (#{@file.length} bytes)</a>
325
+ <input type="file" name="_1-foo" size="" class="file" />
326
+ </span>
327
+ _html
328
+ sd.item('_1', 'foo').get(:action => :update),
329
+ 'File#get should not include a delete submit if the field is mandatory'
330
+ )
331
+ end
332
+
333
+ # TODO: test on all available storages.
334
+ def test_select
335
+ Runo.client = 'root'
336
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
337
+ sd.storage.clear
338
+ sd.update(
339
+ '20100425_1234' => {
340
+ 'foo' => {
341
+ :type => 'image/jpeg',
342
+ :tempfile => @file,
343
+ :filename => 'foo.jpg',
344
+ :name => 't_file'
345
+ },
346
+ }
347
+ ).commit :persistent
348
+
349
+ assert_equal(
350
+ ['20100425_1234'],
351
+ sd.collect {|item| item[:id] },
352
+ 'storages should distinguish between data and files'
353
+ )
354
+ assert_equal(
355
+ ['20100425_1234'],
356
+ sd.instance_eval {
357
+ collect_item(:id => '20100425_1234') {|item| item[:id] }
358
+ },
359
+ 'storages should distinguish between data and files'
360
+ )
361
+ assert_equal(
362
+ ['20100425_1234'],
363
+ sd.instance_eval {
364
+ collect_item(:d => '201004') {|item| item[:id] }
365
+ },
366
+ 'storages should distinguish between data and files'
367
+ )
368
+ assert_equal(
369
+ ['20100425_1234'],
370
+ sd.instance_eval {
371
+ collect_item({}) {|item| item[:id] }
372
+ },
373
+ 'storages should distinguish between data and files'
374
+ )
375
+ end
376
+
377
+ def test_call_body
378
+ Runo.client = 'root'
379
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
380
+ sd.storage.clear
381
+
382
+ # post a multipart request
383
+ input = <<"_eos".gsub(/\r?\n/, "\r\n")
384
+ ---foobarbaz
385
+ Content-Disposition: form-data; name="_1-foo"; filename="foo.jpg"
386
+ Content-Type: image/jpeg
387
+
388
+ #{@file.read}
389
+ ---foobarbaz
390
+ Content-Disposition: form-data; name="_token"
391
+
392
+ #{Runo.token}
393
+ ---foobarbaz--
394
+ _eos
395
+ res = Rack::MockRequest.new(Runo.new).post(
396
+ 'http://example.com/t_file/main/update.html',
397
+ {
398
+ :input => input,
399
+ 'CONTENT_TYPE' => 'multipart/form-data; boundary=-foobarbaz',
400
+ 'CONTENT_LENGTH' => input.length,
401
+ }
402
+ )
403
+ tid = res.headers['Location'][Runo::REX::TID]
404
+
405
+ assert_equal(
406
+ @file.read,
407
+ Runo.transaction[tid].item('_1', 'foo').body,
408
+ 'Runo#call should keep suspended field in Runo.transaction'
409
+ )
410
+
411
+ res = Rack::MockRequest.new(Runo.new).get(
412
+ "http://example.com/#{tid}/_1/foo/foo.jpg"
413
+ )
414
+ assert_equal(
415
+ @file.read,
416
+ res.body,
417
+ 'Runo#call should be able to access suspended file bodies'
418
+ )
419
+
420
+ # commit the base
421
+ res = Rack::MockRequest.new(Runo.new).post(
422
+ "http://example.com/#{tid}/update.html",
423
+ {
424
+ :input => ".status-public=create&_token=#{Runo.token}",
425
+ }
426
+ )
427
+
428
+ res.headers['Location'] =~ Runo::REX::PATH_ID
429
+ new_id = sprintf('%.8d_%.4d', $1, $2)
430
+
431
+ res = Rack::MockRequest.new(Runo.new).get(
432
+ "http://example.com/t_file/#{new_id}/foo/foo.jpg"
433
+ )
434
+ assert_equal(
435
+ 'image/jpeg',
436
+ res.headers['Content-Type'],
437
+ 'Runo#call to a file item should return the mime type of the file'
438
+ )
439
+ assert_equal(
440
+ @file.length.to_s,
441
+ res.headers['Content-Length'],
442
+ 'Runo#call to a file item should return the content length of the file'
443
+ )
444
+ assert_equal(
445
+ @file.read,
446
+ res.body,
447
+ 'Runo#call to a file item should return the binary body of the file'
448
+ )
449
+
450
+ # move
451
+ Rack::MockRequest.new(Runo.new).post(
452
+ 'http://example.com/t_file/update.html',
453
+ {
454
+ :input => "#{new_id}-_timestamp=1981-12-02&.status-public=update&_token=#{Runo.token}",
455
+ }
456
+ )
457
+ res = Rack::MockRequest.new(Runo.new).get(
458
+ "http://example.com/t_file/#{new_id}/foo/foo.jpg"
459
+ )
460
+ assert_equal(
461
+ 404,
462
+ res.status,
463
+ 'Runo#call should move child files as well'
464
+ )
465
+ res = Rack::MockRequest.new(Runo.new).get(
466
+ 'http://example.com/t_file/19811202_0001/foo/foo.jpg'
467
+ )
468
+ assert_equal(
469
+ @file.read,
470
+ res.body,
471
+ 'Runo#call should move child files as well'
472
+ )
473
+
474
+ # delete
475
+ Rack::MockRequest.new(Runo.new).post(
476
+ 'http://example.com/t_file/update.html',
477
+ {
478
+ :input => "19811202_0001.action=delete&.status-public=delete&_token=#{Runo.token}",
479
+ }
480
+ )
481
+ res = Rack::MockRequest.new(Runo.new).get(
482
+ 'http://example.com/t_file/19811202_0001/foo/foo.jpg'
483
+ )
484
+ assert_equal(
485
+ 404,
486
+ res.status,
487
+ 'Runo#call should delete child files as well'
488
+ )
489
+ end
490
+
491
+ def test_errors
492
+ @f.create({})
493
+ @f[:min] = 0
494
+ assert_equal(
495
+ [],
496
+ @f.errors,
497
+ 'File#errors should return the errors of the current body'
498
+ )
499
+
500
+ @f[:min] = 1
501
+ @f.create({})
502
+ assert_equal(
503
+ ['mandatory'],
504
+ @f.errors,
505
+ 'File#errors should return the errors of the current body'
506
+ )
507
+
508
+ @f[:min] = 1
509
+ @f.delete
510
+ @f.commit :temp
511
+ @f.commit :persistent
512
+ assert_equal(
513
+ ['mandatory'],
514
+ @f.errors,
515
+ 'File#errors should return the errors of the current body'
516
+ )
517
+
518
+ @f.update(
519
+ :type => 'image/jpeg',
520
+ :tempfile => @file,
521
+ :head => <<'_eos',
522
+ Content-Disposition: form-data; name="t_file"; filename="baz.jpg"
523
+ Content-Type: image/jpeg
524
+ _eos
525
+ :filename => 'baz.jpg',
526
+ :name => 't_file'
527
+ )
528
+
529
+ @f[:min] = 0
530
+ assert_equal(
531
+ [],
532
+ @f.errors,
533
+ 'File#errors should return the errors of the current body'
534
+ )
535
+
536
+ @f[:min] = @file.length + 1
537
+ @f[:max] = nil
538
+ assert_equal(
539
+ ["too small: #{@file.length + 1} bytes minimum"],
540
+ @f.errors,
541
+ 'File#errors should return the errors of the current body'
542
+ )
543
+
544
+ @f[:min] = nil
545
+ @f[:max] = @file.length - 1
546
+ assert_equal(
547
+ ["too large: #{@file.length - 1} bytes maximum"],
548
+ @f.errors,
549
+ 'File#errors should return the errors of the current body'
550
+ )
551
+
552
+ @f[:min] = nil
553
+ @f[:max] = nil
554
+ @f[:options] = ['txt', 'pdf', 'doc']
555
+ assert_equal(
556
+ ['wrong file type: should be txt/pdf/doc'],
557
+ @f.errors,
558
+ 'File#errors should return the errors of the current body'
559
+ )
560
+ end
561
+
562
+ def test_errors_options
563
+ @f.create(
564
+ :type => 'image/jpeg',
565
+ :tempfile => @file,
566
+ :head => <<'_eos',
567
+ Content-Disposition: form-data; name="t_file"; filename="BAZ.JPG"
568
+ Content-Type: image/jpeg
569
+ _eos
570
+ :filename => 'BAZ.JPG',
571
+ :name => 't_file'
572
+ )
573
+
574
+ @f[:min] = nil
575
+ @f[:max] = nil
576
+ @f[:options] = ['jpg']
577
+ assert_equal(
578
+ [],
579
+ @f.errors,
580
+ 'File#errors should ignore the case of extentions'
581
+ )
582
+ end
583
+
584
+ def test_save_file
585
+ Runo.client = 'root'
586
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
587
+ sd.storage.clear
588
+
589
+ # create a new set with file items
590
+ sd.update(
591
+ '_1' => {
592
+ 'foo' => {
593
+ :type => 'image/jpeg',
594
+ :tempfile => @file,
595
+ :head => <<'_eos',
596
+ Content-Disposition: form-data; name="t_file"; filename="foo.jpg"
597
+ Content-Type: image/jpeg
598
+ _eos
599
+ :filename => 'foo.jpg',
600
+ :name => 't_file'
601
+ },
602
+ }
603
+ )
604
+ assert_nothing_raised(
605
+ 'File#commit should commit files nicely'
606
+ ) {
607
+ sd.commit :persistent
608
+ }
609
+ id = sd.result.values.first[:id]
610
+
611
+ item = Runo::Set::Static::Folder.root.item('t_file', 'main', id, 'foo')
612
+ assert_instance_of(
613
+ Runo::File,
614
+ item,
615
+ 'File#commit should commit the file item'
616
+ )
617
+ assert_equal(
618
+ @file.read,
619
+ item.body,
620
+ 'File#commit should store the body of the file item'
621
+ )
622
+
623
+ # update the sibling file item
624
+ sd.update(
625
+ id => {
626
+ 'bar' => {
627
+ :type => 'image/png',
628
+ :tempfile => @file,
629
+ :head => <<'_eos',
630
+ Content-Disposition: form-data; name="t_file"; filename="bar.png"
631
+ Content-Type: image/png
632
+ _eos
633
+ :filename => 'bar.png',
634
+ :name => 't_file'
635
+ },
636
+ }
637
+ )
638
+ sd.commit :persistent
639
+
640
+ assert_equal(
641
+ @file.read,
642
+ Runo::Set::Static::Folder.root.item('t_file', 'main', id, 'bar').body,
643
+ 'File#commit should store the body of the file item'
644
+ )
645
+ assert_equal(
646
+ @file.read,
647
+ Runo::Set::Static::Folder.root.item('t_file', 'main', id, 'foo').body,
648
+ 'File#commit should keep the body of the untouched file item'
649
+ )
650
+ end
651
+
652
+ def test_delete_file
653
+ Runo.client = 'root'
654
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
655
+ sd.storage.clear
656
+
657
+ sd.update(
658
+ '_1' => {
659
+ 'foo' => {
660
+ :type => 'image/jpeg',
661
+ :tempfile => @file,
662
+ :head => <<'_eos',
663
+ Content-Disposition: form-data; name="t_file"; filename="foo.jpg"
664
+ Content-Type: image/jpeg
665
+ _eos
666
+ :filename => 'foo.jpg',
667
+ :name => 't_file'
668
+ },
669
+ }
670
+ )
671
+ sd.commit :persistent
672
+
673
+ id = sd.result.values.first[:id]
674
+
675
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
676
+ sd.update(
677
+ id => {'foo' => {:action => :delete}}
678
+ )
679
+ assert_equal(:delete, sd.item(id, 'foo').action)
680
+ sd.commit :temp
681
+
682
+ assert_equal(
683
+ {},
684
+ sd.item(id, 'foo').val,
685
+ 'File#delete should clear the val of the field'
686
+ )
687
+ assert_equal(
688
+ :delete,
689
+ sd.item(id, 'foo').action,
690
+ 'File#delete should set @action'
691
+ )
692
+
693
+ another_sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
694
+ assert_not_equal(
695
+ {},
696
+ another_sd.item(id, 'foo').val,
697
+ 'File#delete should not clear the persistent val before commit(:persistent)'
698
+ )
699
+ assert_equal(
700
+ @file.read,
701
+ another_sd.item(id, 'foo').body,
702
+ 'File#delete should not delete the persistent body before commit(:persistent)'
703
+ )
704
+
705
+ sd.commit :persistent
706
+
707
+ another_sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
708
+ assert_equal(
709
+ {},
710
+ another_sd.item(id, 'foo').val,
711
+ 'File#delete should clear the persistent val after commit(:persistent)'
712
+ )
713
+ assert_nil(
714
+ another_sd.item(id, 'foo').body,
715
+ 'File#delete should clear the persistent body after commit(:persistent)'
716
+ )
717
+ end
718
+
719
+ def test_delete_parent_set
720
+ Runo.client = 'root'
721
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
722
+ sd.storage.clear
723
+
724
+ sd.update(
725
+ '_1' => {
726
+ 'foo' => {
727
+ :type => 'image/jpeg',
728
+ :tempfile => @file,
729
+ :head => <<'_eos',
730
+ Content-Disposition: form-data; name="t_file"; filename="foo.jpg"
731
+ Content-Type: image/jpeg
732
+ _eos
733
+ :filename => 'baz.jpg',
734
+ :name => 't_file'
735
+ },
736
+ 'bar' => {
737
+ :type => 'image/jpeg',
738
+ :tempfile => @file,
739
+ :head => <<'_eos',
740
+ Content-Disposition: form-data; name="t_file"; filename="bar.jpg"
741
+ Content-Type: image/jpeg
742
+ _eos
743
+ :filename => 'bar.jpg',
744
+ :name => 't_file'
745
+ },
746
+ }
747
+ )
748
+ sd.commit :persistent
749
+
750
+ id = sd.result.values.first[:id]
751
+
752
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
753
+ sd.update(
754
+ id => {:action => :delete}
755
+ )
756
+ sd.commit :persistent
757
+
758
+ assert_equal(
759
+ {},
760
+ sd.val,
761
+ 'Set#delete should delete body of the file items'
762
+ )
763
+ end
764
+
765
+ def test_save_file_attachment
766
+ Runo.client = 'root'
767
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
768
+ sd.storage.clear
769
+
770
+ # create an attachment file item
771
+ sd.update(
772
+ '_1' => {
773
+ 'baz' => {
774
+ '_1' => {
775
+ 'qux' => {
776
+ :type => 'image/gif',
777
+ :tempfile => @file,
778
+ :head => <<'_eos',
779
+ Content-Disposition: form-data; name="t_file"; filename="qux.gif"
780
+ Content-Type: image/gif
781
+ _eos
782
+ :filename => 'qux.gif',
783
+ :name => 't_file'
784
+ },
785
+ },
786
+ }
787
+ }
788
+ )
789
+
790
+ sd.commit :persistent
791
+ baz_id = sd.result.values.first[:id]
792
+ qux_id = sd.result.values.first.item('baz').val.keys.first
793
+
794
+ item = Runo::Set::Static::Folder.root.item('t_file', 'main', baz_id, 'baz', qux_id, 'qux')
795
+ assert_instance_of(
796
+ Runo::File,
797
+ item,
798
+ 'File#commit should commit the attachment file item'
799
+ )
800
+ assert_equal(
801
+ @file.read,
802
+ item.body,
803
+ 'File#commit should store the body of the attachment file item'
804
+ )
805
+
806
+ # delete the attachment
807
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
808
+ sd.update(
809
+ baz_id => {
810
+ 'baz' => {
811
+ qux_id => {:action => :delete},
812
+ }
813
+ }
814
+ )
815
+ sd.commit :persistent
816
+
817
+ assert_equal(
818
+ {},
819
+ Runo::Set::Static::Folder.root.item('t_file', 'main', baz_id, 'baz').storage.val,
820
+ 'File#commit should delete the body of the attachment file item'
821
+ )
822
+ end
823
+
824
+ def test_delete_attachment_parent
825
+ Runo.client = 'root'
826
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
827
+ sd.storage.clear
828
+
829
+ # create an attachment file item
830
+ sd.update(
831
+ '_1' => {
832
+ 'baz' => {
833
+ '_1' => {
834
+ 'qux' => {
835
+ :type => 'image/gif',
836
+ :tempfile => @file,
837
+ :head => <<'_eos',
838
+ Content-Disposition: form-data; name="t_file"; filename="qux.gif"
839
+ Content-Type: image/gif
840
+ _eos
841
+ :filename => 'qux.gif',
842
+ :name => 't_file'
843
+ },
844
+ },
845
+ }
846
+ }
847
+ )
848
+
849
+ sd.commit :persistent
850
+ assert_not_equal({}, sd.val)
851
+
852
+ baz_id = sd.result.values.first[:id]
853
+ qux_id = sd.result.values.first.item('baz').val.keys.first
854
+
855
+ item = Runo::Set::Static::Folder.root.item('t_file', 'main', baz_id, 'baz', qux_id, 'qux')
856
+ item_persistent_name = item[:persistent_name]
857
+ assert_equal(
858
+ @file.read,
859
+ Runo::Set::Static::Folder.root.item('t_file', 'main').storage.val(item_persistent_name),
860
+ 'the body of the file should be stored in the storage'
861
+ )
862
+
863
+ # delete the parent set
864
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
865
+ sd[:item]['default'][:item].delete '_timestamp'
866
+ sd.update(
867
+ baz_id => {:action => :delete}
868
+ )
869
+
870
+ sd.commit :temp
871
+ assert_equal(
872
+ <<"_eos",
873
+ <"main" @action=:update @result={"#{baz_id}"}>
874
+ <"#{baz_id}" @action=:delete @result=:delete>
875
+ <"_group" @action=nil @result=nil @val=nil>
876
+ <"_owner" @action=nil @result=nil @val="root">
877
+ <"bar" @action=:delete @result=:delete @val={}>
878
+ <"baz" @action=:delete @result=:delete>
879
+ <"foo" @action=:delete @result=:delete @val={}>
880
+ _eos
881
+ sd.inspect_items
882
+ )
883
+
884
+ sd.commit :persistent
885
+ assert_equal(
886
+ <<"_eos",
887
+ <"main" @action=nil @result={"#{baz_id}"}>
888
+ _eos
889
+ sd.inspect_items
890
+ )
891
+
892
+ sd = Runo::Set::Static::Folder.root.item('t_file', 'main')
893
+ assert_equal({}, sd.val)
894
+ assert_nil(
895
+ Runo::Set::Static::Folder.root.item('t_file', 'main').storage.val(item_persistent_name),
896
+ 'the body of the file should be deleted from the storage'
897
+ )
898
+ end
899
+
900
+ end