reform 2.2.4 → 2.3.3

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 (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