reform 2.2.4 → 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +5 -1
  3. data/.travis.yml +11 -6
  4. data/Appraisals +8 -0
  5. data/CHANGES.md +57 -4
  6. data/CONTRIBUTING.md +31 -0
  7. data/Gemfile +2 -16
  8. data/ISSUE_TEMPLATE.md +25 -0
  9. data/LICENSE.txt +1 -1
  10. data/README.md +5 -7
  11. data/Rakefile +16 -9
  12. data/gemfiles/0.13.0.gemfile +8 -0
  13. data/gemfiles/1.5.0.gemfile +9 -0
  14. data/lib/reform.rb +1 -0
  15. data/lib/reform/contract.rb +7 -17
  16. data/lib/reform/contract/custom_error.rb +41 -0
  17. data/lib/reform/contract/validate.rb +53 -23
  18. data/lib/reform/errors.rb +61 -0
  19. data/lib/reform/form.rb +36 -10
  20. data/lib/reform/form/call.rb +1 -1
  21. data/lib/reform/form/composition.rb +2 -2
  22. data/lib/reform/form/dry.rb +10 -58
  23. data/lib/reform/form/dry/input_hash.rb +37 -0
  24. data/lib/reform/form/dry/new_api.rb +45 -0
  25. data/lib/reform/form/dry/old_api.rb +61 -0
  26. data/lib/reform/form/populator.rb +11 -27
  27. data/lib/reform/form/prepopulate.rb +4 -3
  28. data/lib/reform/form/validate.rb +28 -13
  29. data/lib/reform/result.rb +90 -0
  30. data/lib/reform/validation.rb +19 -11
  31. data/lib/reform/validation/groups.rb +12 -27
  32. data/lib/reform/version.rb +1 -1
  33. data/reform.gemspec +14 -13
  34. data/test/benchmarking.rb +39 -6
  35. data/test/call_new_api.rb +23 -0
  36. data/test/call_old_api.rb +23 -0
  37. data/test/changed_test.rb +14 -14
  38. data/test/coercion_test.rb +57 -25
  39. data/test/composition_new_api.rb +186 -0
  40. data/test/composition_old_api.rb +184 -0
  41. data/test/contract/custom_error_test.rb +55 -0
  42. data/test/contract_new_api.rb +77 -0
  43. data/test/contract_old_api.rb +77 -0
  44. data/test/default_test.rb +4 -4
  45. data/test/deserialize_test.rb +17 -20
  46. data/test/errors_new_api.rb +225 -0
  47. data/test/errors_old_api.rb +230 -0
  48. data/test/feature_test.rb +10 -12
  49. data/test/fixtures/dry_error_messages.yml +73 -23
  50. data/test/fixtures/dry_new_api_error_messages.yml +104 -0
  51. data/test/form_new_api.rb +57 -0
  52. data/test/{form_test.rb → form_old_api.rb} +8 -8
  53. data/test/form_option_new_api.rb +24 -0
  54. data/test/{form_option_test.rb → form_option_old_api.rb} +5 -5
  55. data/test/from_test.rb +18 -22
  56. data/test/inherit_new_api.rb +105 -0
  57. data/test/inherit_old_api.rb +105 -0
  58. data/test/{module_test.rb → module_new_api.rb} +26 -31
  59. data/test/module_old_api.rb +146 -0
  60. data/test/parse_option_test.rb +40 -0
  61. data/test/parse_pipeline_test.rb +4 -4
  62. data/test/populate_new_api.rb +304 -0
  63. data/test/populate_old_api.rb +304 -0
  64. data/test/populator_skip_test.rb +11 -11
  65. data/test/prepopulator_test.rb +23 -24
  66. data/test/read_only_test.rb +12 -1
  67. data/test/readable_test.rb +9 -9
  68. data/test/reform_new_api.rb +204 -0
  69. data/test/{reform_test.rb → reform_old_api.rb} +44 -65
  70. data/test/save_new_api.rb +101 -0
  71. data/test/save_old_api.rb +101 -0
  72. data/test/setup_test.rb +17 -17
  73. data/test/skip_if_new_api.rb +85 -0
  74. data/test/skip_if_old_api.rb +92 -0
  75. data/test/skip_setter_and_getter_test.rb +9 -10
  76. data/test/test_helper.rb +25 -14
  77. data/test/validate_new_api.rb +453 -0
  78. data/test/{validate_test.rb → validate_old_api.rb} +121 -131
  79. data/test/validation/dry_validation_new_api.rb +835 -0
  80. data/test/validation/dry_validation_old_api.rb +772 -0
  81. data/test/validation/result_test.rb +77 -0
  82. data/test/validation_library_provided_test.rb +16 -0
  83. data/test/virtual_test.rb +47 -7
  84. data/test/writeable_test.rb +38 -9
  85. metadata +111 -56
  86. data/gemfiles/Gemfile.disposable-0.3 +0 -6
  87. data/lib/reform/contract/errors.rb +0 -43
  88. data/lib/reform/form/mongoid.rb +0 -37
  89. data/lib/reform/form/orm.rb +0 -26
  90. data/lib/reform/mongoid.rb +0 -4
  91. data/test/call_test.rb +0 -23
  92. data/test/composition_test.rb +0 -149
  93. data/test/contract_test.rb +0 -77
  94. data/test/deprecation_test.rb +0 -27
  95. data/test/errors_test.rb +0 -165
  96. data/test/inherit_test.rb +0 -119
  97. data/test/populate_test.rb +0 -270
  98. data/test/readonly_test.rb +0 -14
  99. data/test/save_test.rb +0 -89
  100. data/test/skip_if_test.rb +0 -74
  101. data/test/validation/dry_test.rb +0 -60
  102. data/test/validation/dry_validation_test.rb +0 -352
  103. data/test/validation/errors.yml +0 -4
@@ -1,11 +1,32 @@
1
1
  require "reform"
2
- require 'minitest/autorun'
2
+ require "minitest/autorun"
3
3
  require "representable/debug"
4
4
  require "declarative/testing"
5
5
  require "pp"
6
+ require "pry-byebug"
7
+
8
+ require "reform/form/dry"
9
+
10
+ # setup test classes so we can test without dry being included
11
+ class TestForm < Reform::Form
12
+ feature Reform::Form::Dry
13
+ end
14
+
15
+ class TestContract < Reform::Contract
16
+ feature Reform::Form::Dry
17
+ end
18
+
19
+ module Types
20
+ DRY_MODULE = Gem::Version.new(Dry::Types::VERSION) < Gem::Version.new("0.15.0") ? Dry::Types.module : Dry.Types()
21
+ include DRY_MODULE
22
+ end
23
+
24
+ DRY_TYPES_VERSION = Gem::Version.new(Dry::Types::VERSION)
25
+ DRY_TYPES_CONSTANT = DRY_TYPES_VERSION < Gem::Version.new("0.13.0") ? Types::Form : Types::Params
26
+ DRY_TYPES_INT_CONSTANT = DRY_TYPES_VERSION < Gem::Version.new("0.13.0") ? Types::Form::Int : Types::Params::Integer
6
27
 
7
28
  class BaseTest < MiniTest::Spec
8
- class AlbumForm < Reform::Form
29
+ class AlbumForm < TestForm
9
30
  property :title
10
31
 
11
32
  property :hit do
@@ -17,14 +38,13 @@ class BaseTest < MiniTest::Spec
17
38
  end
18
39
  end
19
40
 
20
- Song = Struct.new(:title, :length)
41
+ Song = Struct.new(:title, :length, :rating)
21
42
  Album = Struct.new(:title, :hit, :songs, :band)
22
43
  Band = Struct.new(:label)
23
44
  Label = Struct.new(:name)
24
45
  Length = Struct.new(:minutes, :seconds)
25
46
 
26
-
27
- let (:hit) { Song.new("Roxanne") }
47
+ let(:hit) { Song.new("Roxanne") }
28
48
  end
29
49
 
30
50
  MiniTest::Spec.class_eval do
@@ -38,12 +58,3 @@ MiniTest::Spec.class_eval do
38
58
  end
39
59
  end
40
60
  end
41
-
42
- require "reform/form/dry"
43
- Reform::Contract.class_eval do
44
- feature Reform::Form::Dry
45
- end
46
- # FIXME!
47
- Reform::Form.class_eval do
48
- feature Reform::Form::Dry
49
- end
@@ -0,0 +1,453 @@
1
+ require "test_helper"
2
+
3
+ class ContractValidateTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ class AlbumForm < TestContract
9
+ property :name
10
+ validation do
11
+ params { required(:name).filled }
12
+ end
13
+
14
+ collection :songs do
15
+ property :title
16
+ validation do
17
+ params { required(:title).filled }
18
+ end
19
+
20
+ property :composer do
21
+ validation do
22
+ params { required(:name).filled }
23
+ end
24
+ property :name
25
+ end
26
+ end
27
+
28
+ property :artist do
29
+ property :name
30
+ end
31
+ end
32
+
33
+ let(:song) { Song.new("Broken") }
34
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
35
+ let(:composer) { Artist.new("Greg Graffin") }
36
+ let(:artist) { Artist.new("Bad Religion") }
37
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
38
+
39
+ let(:form) { AlbumForm.new(album) }
40
+
41
+ # valid
42
+ it do
43
+ _(form.validate).must_equal true
44
+ _(form.errors.messages.inspect).must_equal "{}"
45
+ end
46
+
47
+ # invalid
48
+ it do
49
+ album.songs[1].composer.name = nil
50
+ album.name = nil
51
+
52
+ _(form.validate).must_equal false
53
+ _(form.errors.messages.inspect).must_equal "{:name=>[\"must be filled\"], :\"songs.composer.name\"=>[\"must be filled\"]}"
54
+ end
55
+ end
56
+
57
+ # no configuration results in "sync" (formerly known as parse_strategy: :sync).
58
+ class ValidateWithoutConfigurationTest < MiniTest::Spec
59
+ Song = Struct.new(:title, :album, :composer)
60
+ Album = Struct.new(:name, :songs, :artist)
61
+ Artist = Struct.new(:name)
62
+
63
+ class AlbumForm < TestForm
64
+ property :name
65
+ validation do
66
+ params { required(:name).filled }
67
+ end
68
+
69
+ collection :songs do
70
+ property :title
71
+ validation do
72
+ params { required(:title).filled }
73
+ end
74
+
75
+ property :composer do
76
+ property :name
77
+ validation do
78
+ params { required(:name).filled }
79
+ end
80
+ end
81
+ end
82
+
83
+ property :artist do
84
+ property :name
85
+ end
86
+ end
87
+
88
+ let(:song) { Song.new("Broken") }
89
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
90
+ let(:composer) { Artist.new("Greg Graffin") }
91
+ let(:artist) { Artist.new("Bad Religion") }
92
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
93
+
94
+ let(:form) { AlbumForm.new(album) }
95
+
96
+ # valid.
97
+ it do
98
+ object_ids = {
99
+ song: form.songs[0].object_id, song_with_composer: form.songs[1].object_id,
100
+ artist: form.artist.object_id, composer: form.songs[1].composer.object_id
101
+ }
102
+
103
+ _(form.validate(
104
+ "name" => "Best Of",
105
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne", "composer" => {"name" => "Sting"}}],
106
+ "artist" => {"name" => "The Police"}
107
+ )).must_equal true
108
+
109
+ _(form.errors.messages.inspect).must_equal "{}"
110
+
111
+ # form has updated.
112
+ _(form.name).must_equal "Best Of"
113
+ _(form.songs[0].title).must_equal "Fallout"
114
+ _(form.songs[1].title).must_equal "Roxanne"
115
+ _(form.songs[1].composer.name).must_equal "Sting"
116
+ _(form.artist.name).must_equal "The Police"
117
+
118
+ # objects are still the same.
119
+ _(form.songs[0].object_id).must_equal object_ids[:song]
120
+ _(form.songs[1].object_id).must_equal object_ids[:song_with_composer]
121
+ _(form.songs[1].composer.object_id).must_equal object_ids[:composer]
122
+ _(form.artist.object_id).must_equal object_ids[:artist]
123
+
124
+ # model has not changed, yet.
125
+ _(album.name).must_equal "The Dissent Of Man"
126
+ _(album.songs[0].title).must_equal "Broken"
127
+ _(album.songs[1].title).must_equal "Resist Stance"
128
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
129
+ _(album.artist.name).must_equal "Bad Religion"
130
+ end
131
+
132
+ # with symbols.
133
+ it do
134
+ _(form.validate(
135
+ name: "Best Of",
136
+ songs: [{title: "The X-Creep"}, {title: "Trudging", composer: {name: "SNFU"}}],
137
+ artist: {name: "The Police"}
138
+ )).must_equal true
139
+
140
+ _(form.name).must_equal "Best Of"
141
+ _(form.songs[0].title).must_equal "The X-Creep"
142
+ _(form.songs[1].title).must_equal "Trudging"
143
+ _(form.songs[1].composer.name).must_equal "SNFU"
144
+ _(form.artist.name).must_equal "The Police"
145
+ end
146
+
147
+ # throws exception when no populators.
148
+ it do
149
+ album = Album.new("The Dissent Of Man", [])
150
+
151
+ assert_raises RuntimeError do
152
+ AlbumForm.new(album).validate(songs: {title: "Resist-Stance"})
153
+ end
154
+ end
155
+ end
156
+
157
+ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
158
+ Song = Struct.new(:title, :album, :composer)
159
+ Album = Struct.new(:name, :songs, :artist)
160
+ Artist = Struct.new(:name)
161
+
162
+ class AlbumForm < TestForm
163
+ property :name
164
+ validation do
165
+ params { required(:name).filled }
166
+ end
167
+
168
+ collection :songs,
169
+ internal_populator: ->(input, options) {
170
+ collection = options[:represented].songs
171
+ (item = collection[options[:index]]) ? item : collection.insert(options[:index], Song.new)
172
+ } do
173
+ property :title
174
+ validation do
175
+ params { required(:title).filled }
176
+ end
177
+
178
+ property :composer, internal_populator: ->(input, options) { (item = options[:represented].composer) ? item : Artist.new } do
179
+ property :name
180
+ validation do
181
+ params { required(:name).filled }
182
+ end
183
+ end
184
+ end
185
+
186
+ property :artist, internal_populator: ->(input, options) { (item = options[:represented].artist) ? item : Artist.new } do
187
+ property :name
188
+ validation do
189
+ params { required(:name).filled }
190
+ end
191
+ end
192
+ end
193
+
194
+ let(:song) { Song.new("Broken") }
195
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
196
+ let(:composer) { Artist.new("Greg Graffin") }
197
+ let(:artist) { Artist.new("Bad Religion") }
198
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
199
+
200
+ let(:form) { AlbumForm.new(album) }
201
+
202
+ # valid.
203
+ it("xxx") do
204
+ _(form.validate(
205
+ "name" => "Best Of",
206
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne", "composer" => {"name" => "Sting"}}],
207
+ "artist" => {"name" => "The Police"},
208
+ )).must_equal true
209
+
210
+ _(form.errors.messages.inspect).must_equal "{}"
211
+
212
+ # form has updated.
213
+ _(form.name).must_equal "Best Of"
214
+ _(form.songs[0].title).must_equal "Fallout"
215
+ _(form.songs[1].title).must_equal "Roxanne"
216
+ _(form.songs[1].composer.name).must_equal "Sting"
217
+ _(form.artist.name).must_equal "The Police"
218
+
219
+ # model has not changed, yet.
220
+ _(album.name).must_equal "The Dissent Of Man"
221
+ _(album.songs[0].title).must_equal "Broken"
222
+ _(album.songs[1].title).must_equal "Resist Stance"
223
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
224
+ _(album.artist.name).must_equal "Bad Religion"
225
+ end
226
+
227
+ # invalid.
228
+ it do
229
+ _(form.validate(
230
+ "name" => "",
231
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne", "composer" => {"name" => ""}}],
232
+ "artist" => {"name" => ""},
233
+ )).must_equal false
234
+
235
+ _(form.errors.messages.inspect).must_equal "{:name=>[\"must be filled\"], :\"songs.composer.name\"=>[\"must be filled\"], :\"artist.name\"=>[\"must be filled\"]}"
236
+ end
237
+
238
+ # adding to collection via :instance.
239
+ # valid.
240
+ it do
241
+ _(form.validate(
242
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"}, {"title" => "Rime Of The Ancient Mariner"}],
243
+ )).must_equal true
244
+
245
+ _(form.errors.messages.inspect).must_equal "{}"
246
+
247
+ # form has updated.
248
+ _(form.name).must_equal "The Dissent Of Man"
249
+ _(form.songs[0].title).must_equal "Fallout"
250
+ _(form.songs[1].title).must_equal "Roxanne"
251
+ _(form.songs[1].composer.name).must_equal "Greg Graffin"
252
+ _(form.songs[1].title).must_equal "Roxanne"
253
+ _(form.songs[2].title).must_equal "Rime Of The Ancient Mariner" # new song added.
254
+ _(form.songs.size).must_equal 3
255
+ _(form.artist.name).must_equal "Bad Religion"
256
+
257
+ # model has not changed, yet.
258
+ _(album.name).must_equal "The Dissent Of Man"
259
+ _(album.songs[0].title).must_equal "Broken"
260
+ _(album.songs[1].title).must_equal "Resist Stance"
261
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
262
+ _(album.songs.size).must_equal 2
263
+ _(album.artist.name).must_equal "Bad Religion"
264
+ end
265
+
266
+ # allow writeable: false even in the deserializer.
267
+ class SongForm < TestForm
268
+ property :title, deserializer: {writeable: false}
269
+ end
270
+
271
+ it do
272
+ form = SongForm.new(song = Song.new)
273
+ form.validate("title" => "Ignore me!")
274
+ assert_nil form.title
275
+ form.title = "Unopened"
276
+ form.sync # only the deserializer is marked as not-writeable.
277
+ _(song.title).must_equal "Unopened"
278
+ end
279
+ end
280
+
281
+ class ValidateUsingDifferentFormObject < MiniTest::Spec
282
+ Album = Struct.new(:name)
283
+
284
+ class AlbumForm < TestForm
285
+ property :name
286
+
287
+ validation do
288
+ option :form
289
+
290
+ params { required(:name).filled(:str?) }
291
+
292
+ rule(:name) do
293
+ if form.name == 'invalid'
294
+ key.failure('Invalid name')
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ let(:album) { Album.new }
301
+
302
+ let(:form) { AlbumForm.new(album) }
303
+
304
+ it 'sets name correctly' do
305
+ assert form.validate(name: 'valid')
306
+ form.sync
307
+ assert_equal form.model.name, 'valid'
308
+ end
309
+
310
+ it 'validates presence of name' do
311
+ refute form.validate(name: nil)
312
+ assert_equal form.errors[:name], ["must be filled"]
313
+ end
314
+
315
+ it 'validates type of name' do
316
+ refute form.validate(name: 1)
317
+ assert_equal form.errors[:name], ["must be a string"]
318
+ end
319
+
320
+ it 'when name is invalid' do
321
+ refute form.validate(name: 'invalid')
322
+ assert_equal form.errors[:name], ["Invalid name"]
323
+ end
324
+ end
325
+
326
+ # # not sure if we should catch that in Reform or rather do that in disposable. this is https://github.com/trailblazer/reform/pull/104
327
+ # # describe ":populator with :empty" do
328
+ # # let(:form) {
329
+ # # Class.new(Reform::Form) do
330
+ # # collection :songs, :empty => true, :populator => lambda { |fragment, index, args|
331
+ # # songs[index] = args.binding[:form].new(Song.new)
332
+ # # } do
333
+ # # property :title
334
+ # # end
335
+ # # end
336
+ # # }
337
+
338
+ # # let(:params) {
339
+ # # {
340
+ # # "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"}]
341
+ # # }
342
+ # # }
343
+
344
+ # # subject { form.new(Album.new("Hits", [], [])) }
345
+
346
+ # # before { subject.validate(params) }
347
+
348
+ # # it { subject.songs[0].title.must_equal "Fallout" }
349
+ # # it { subject.songs[1].title.must_equal "Roxanne" }
350
+ # # end
351
+
352
+ # # test cardinalities.
353
+ # describe "with empty collection and cardinality" do
354
+ # let(:album) { Album.new }
355
+
356
+ # subject { Class.new(Reform::Form) do
357
+ # include Reform::Form::ActiveModel
358
+ # model :album
359
+
360
+ # collection :songs do
361
+ # property :title
362
+ # end
363
+
364
+ # property :hit do
365
+ # property :title
366
+ # end
367
+
368
+ # validates :songs, :length => {:minimum => 1}
369
+ # validates :hit, :presence => true
370
+ # end.new(album) }
371
+
372
+ # describe "invalid" do
373
+ # before { subject.validate({}).must_equal false }
374
+
375
+ # it do
376
+ # # ensure that only hit and songs keys are present
377
+ # subject.errors.messages.keys.sort.must_equal([:hit, :songs])
378
+ # # validate content of hit and songs keys
379
+ # subject.errors.messages[:hit].must_equal(["must be filled"])
380
+ # subject.errors.messages[:songs].first.must_match(/\Ais too short \(minimum is 1 characters?\)\z/)
381
+ # end
382
+ # end
383
+
384
+ # describe "valid" do
385
+ # let(:album) { Album.new(nil, Song.new, [Song.new("Urban Myth")]) }
386
+
387
+ # before {
388
+ # subject.validate({"songs" => [{"title"=>"Daddy, Brother, Lover, Little Boy"}], "hit" => {"title"=>"The Horse"}}).
389
+ # must_equal true
390
+ # }
391
+
392
+ # it { subject.errors.messages.must_equal({}) }
393
+ # end
394
+ # end
395
+
396
+ # # providing manual validator method allows accessing form's API.
397
+ # describe "with ::validate" do
398
+ # let(:form) {
399
+ # Class.new(Reform::Form) do
400
+ # property :title
401
+
402
+ # validate :title?
403
+
404
+ # def title?
405
+ # errors.add :title, "not lowercase" if title == "Fallout"
406
+ # end
407
+ # end
408
+ # }
409
+
410
+ # let(:params) { {"title" => "Fallout"} }
411
+ # let(:song) { Song.new("Englishman") }
412
+
413
+ # subject { form.new(song) }
414
+
415
+ # before { @res = subject.validate(params) }
416
+
417
+ # it { @res.must_equal false }
418
+ # it { subject.errors.messages.must_equal({:title=>["not lowercase"]}) }
419
+ # end
420
+
421
+ # # overriding the reader for a nested form should only be considered when rendering.
422
+ # describe "with overridden reader for nested form" do
423
+ # let(:form) {
424
+ # Class.new(Reform::Form) do
425
+ # property :band, :populate_if_empty => lambda { |*| Band.new } do
426
+ # property :label
427
+ # end
428
+
429
+ # collection :songs, :populate_if_empty => lambda { |*| Song.new } do
430
+ # property :title
431
+ # end
432
+
433
+ # def band
434
+ # raise "only call me when rendering the form!"
435
+ # end
436
+
437
+ # def songs
438
+ # raise "only call me when rendering the form!"
439
+ # end
440
+ # end.new(album)
441
+ # }
442
+
443
+ # let(:album) { Album.new }
444
+
445
+ # # don't use #artist when validating!
446
+ # it do
447
+ # form.validate("band" => {"label" => "Hellcat"}, "songs" => [{"title" => "Stand Your Ground"}, {"title" => "Otherside"}])
448
+ # form.sync
449
+ # album.band.label.must_equal "Hellcat"
450
+ # album.songs.first.title.must_equal "Stand Your Ground"
451
+ # end
452
+ # end
453
+ # end