trailblazer 0.3.3 → 1.0.0.rc1
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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGES.md +12 -0
- data/Gemfile +2 -1
- data/README.md +73 -38
- data/Rakefile +1 -1
- data/lib/trailblazer/autoloading.rb +4 -1
- data/lib/trailblazer/endpoint.rb +7 -13
- data/lib/trailblazer/operation.rb +54 -40
- data/lib/trailblazer/operation/builder.rb +26 -0
- data/lib/trailblazer/operation/collection.rb +1 -2
- data/lib/trailblazer/operation/controller.rb +36 -48
- data/lib/trailblazer/operation/dispatch.rb +11 -11
- data/lib/trailblazer/operation/model.rb +50 -0
- data/lib/trailblazer/operation/model/dsl.rb +29 -0
- data/lib/trailblazer/operation/model/external.rb +34 -0
- data/lib/trailblazer/operation/policy.rb +87 -0
- data/lib/trailblazer/operation/policy/guard.rb +34 -0
- data/lib/trailblazer/operation/representer.rb +33 -12
- data/lib/trailblazer/operation/resolver.rb +30 -0
- data/lib/trailblazer/operation/responder.rb +0 -1
- data/lib/trailblazer/operation/worker.rb +24 -7
- data/lib/trailblazer/version.rb +1 -1
- data/test/collection_test.rb +2 -1
- data/test/{crud_test.rb → model_test.rb} +17 -35
- data/test/operation/builder_test.rb +41 -0
- data/test/operation/dsl/callback_test.rb +108 -0
- data/test/operation/dsl/contract_test.rb +104 -0
- data/test/operation/dsl/representer_test.rb +143 -0
- data/test/operation/external_model_test.rb +71 -0
- data/test/operation/guard_test.rb +97 -0
- data/test/operation/policy_test.rb +97 -0
- data/test/operation/resolver_test.rb +83 -0
- data/test/operation_test.rb +7 -75
- data/test/rails/__respond_test.rb +20 -0
- data/test/rails/controller_test.rb +4 -102
- data/test/rails/endpoint_test.rb +7 -47
- data/test/rails/fake_app/controllers.rb +16 -21
- data/test/rails/fake_app/rails_app.rb +5 -0
- data/test/rails/fake_app/song/operations.rb +11 -4
- data/test/rails/respond_test.rb +95 -0
- data/test/responder_test.rb +6 -6
- data/test/rollback_test.rb +2 -2
- data/test/worker_test.rb +13 -9
- data/trailblazer.gemspec +2 -2
- metadata +38 -15
- data/lib/trailblazer/operation/crud.rb +0 -82
- data/lib/trailblazer/rails/railtie.rb +0 -34
- data/test/rails/fake_app/views/bands/show.html.erb +0 -1
@@ -0,0 +1,30 @@
|
|
1
|
+
require "trailblazer/operation/model/external"
|
2
|
+
require "trailblazer/operation/policy"
|
3
|
+
|
4
|
+
class Trailblazer::Operation
|
5
|
+
# Provides builds-> (model, policy, params).
|
6
|
+
module Resolver
|
7
|
+
def self.included(includer)
|
8
|
+
includer.class_eval do
|
9
|
+
include Policy # ::build_policy
|
10
|
+
include Model::External # ::build_operation_class
|
11
|
+
|
12
|
+
extend BuildOperation # ::build_operation
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module BuildOperation
|
17
|
+
def build_operation(params, options={})
|
18
|
+
model = model!(params)
|
19
|
+
policy = policy_config.call(params[:current_user], model)
|
20
|
+
build_operation_class(model, policy, params).
|
21
|
+
new(params, options.merge(model: model, policy: policy))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(params, options)
|
26
|
+
@policy = options[:policy]
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -7,7 +7,7 @@ class Trailblazer::Operation
|
|
7
7
|
# Works with Reform 2, only.
|
8
8
|
module Worker
|
9
9
|
def self.included(base)
|
10
|
-
base.send(:include, Sidekiq::Worker)
|
10
|
+
base.send(:include, Sidekiq::Worker)
|
11
11
|
base.extend(ClassMethods)
|
12
12
|
end
|
13
13
|
|
@@ -17,10 +17,28 @@ class Trailblazer::Operation
|
|
17
17
|
return perform_async(serializable(params))
|
18
18
|
end
|
19
19
|
|
20
|
-
|
20
|
+
super(params)
|
21
|
+
end
|
22
|
+
|
23
|
+
def new(*args)
|
24
|
+
return super if args.any?
|
25
|
+
# sidekiq behavior: (not a big fan of this)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def perform(params) # called by Sidekiq.
|
30
|
+
build_operation(params).perform
|
31
|
+
end
|
32
|
+
|
33
|
+
def jid=(jid)
|
34
|
+
puts "@@@@@ #{jid.inspect}"
|
21
35
|
end
|
22
36
|
|
23
37
|
private
|
38
|
+
def perform_async(*args)
|
39
|
+
client_push('class' => self, 'args' => args) # calls class.new.perform(params)
|
40
|
+
end
|
41
|
+
|
24
42
|
def background? # TODO: make configurable.
|
25
43
|
true
|
26
44
|
# if Rails.env == "production" or Rails.env == "staging"
|
@@ -32,16 +50,15 @@ class Trailblazer::Operation
|
|
32
50
|
end
|
33
51
|
|
34
52
|
|
35
|
-
#
|
36
|
-
def perform(params)
|
53
|
+
def perform#(params)
|
37
54
|
# the serialized params hash from Sidekiq contains a Op::UploadedFile hash.
|
38
55
|
|
39
56
|
# the following code is basically what happens in a controller.
|
40
57
|
# this is a bug in Rails, it doesn't work without requiring as/hash/ina
|
41
58
|
# params = ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(params) # TODO: this might make it ultra-slow as Reform converts it back to strings.
|
42
|
-
params = params.with_indifferent_access
|
43
|
-
|
44
|
-
run
|
59
|
+
params = @params.with_indifferent_access
|
60
|
+
@params = deserializable(params)
|
61
|
+
run
|
45
62
|
end
|
46
63
|
|
47
64
|
private
|
data/lib/trailblazer/version.rb
CHANGED
data/test/collection_test.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
require "trailblazer/operation/collection"
|
3
|
+
require "trailblazer/operation/model"
|
3
4
|
|
4
5
|
class CollectionTest < MiniTest::Spec
|
5
6
|
Song = Struct.new(:title, :id) do
|
@@ -14,7 +15,7 @@ class CollectionTest < MiniTest::Spec
|
|
14
15
|
|
15
16
|
|
16
17
|
class CreateOperation < Trailblazer::Operation
|
17
|
-
include
|
18
|
+
include Model
|
18
19
|
model Song
|
19
20
|
action :create
|
20
21
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'trailblazer/operation'
|
3
3
|
|
4
|
-
class
|
4
|
+
class ModelTest < MiniTest::Spec
|
5
5
|
Song = Struct.new(:title, :id) do
|
6
6
|
class << self
|
7
7
|
attr_accessor :find_result # TODO: eventually, replace with AR test.
|
@@ -14,7 +14,7 @@ class CrudTest < MiniTest::Spec
|
|
14
14
|
end
|
15
15
|
|
16
16
|
class CreateOperation < Trailblazer::Operation
|
17
|
-
include
|
17
|
+
include Model
|
18
18
|
model Song
|
19
19
|
action :create
|
20
20
|
|
@@ -32,9 +32,9 @@ class CrudTest < MiniTest::Spec
|
|
32
32
|
|
33
33
|
|
34
34
|
# creates model for you.
|
35
|
-
it { CreateOperation
|
35
|
+
it { CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
|
36
36
|
# exposes #model.
|
37
|
-
it { CreateOperation
|
37
|
+
it { CreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.must_be_instance_of Song }
|
38
38
|
|
39
39
|
class ModifyingCreateOperation < CreateOperation
|
40
40
|
def process(params)
|
@@ -47,8 +47,8 @@ class CrudTest < MiniTest::Spec
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# lets you modify model.
|
50
|
-
it { ModifyingCreateOperation
|
51
|
-
it { ModifyingCreateOperation
|
50
|
+
it { ModifyingCreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.title.must_equal "Blue Rondo a la Turk" }
|
51
|
+
it { ModifyingCreateOperation.(song: {title: "Blue Rondo a la Turk"}).model.genre.must_equal "Punkrock" }
|
52
52
|
|
53
53
|
# Update
|
54
54
|
class UpdateOperation < CreateOperation
|
@@ -57,10 +57,10 @@ class CrudTest < MiniTest::Spec
|
|
57
57
|
|
58
58
|
# finds model and updates.
|
59
59
|
it do
|
60
|
-
song = CreateOperation
|
60
|
+
song = CreateOperation.(song: {title: "Anchor End"}).model
|
61
61
|
Song.find_result = song
|
62
62
|
|
63
|
-
UpdateOperation
|
63
|
+
UpdateOperation.(id: song.id, song: {title: "The Rip"}).model.title.must_equal "The Rip"
|
64
64
|
song.title.must_equal "The Rip"
|
65
65
|
end
|
66
66
|
|
@@ -71,16 +71,16 @@ class CrudTest < MiniTest::Spec
|
|
71
71
|
|
72
72
|
# finds model and updates.
|
73
73
|
it do
|
74
|
-
song = CreateOperation
|
74
|
+
song = CreateOperation.(song: {title: "Anchor End"}).model
|
75
75
|
Song.find_result = song
|
76
76
|
|
77
|
-
FindOperation
|
77
|
+
FindOperation.(id: song.id, song: {title: "The Rip"}).model.title.must_equal "The Rip"
|
78
78
|
song.title.must_equal "The Rip"
|
79
79
|
end
|
80
80
|
|
81
81
|
|
82
82
|
class DefaultCreateOperation < Trailblazer::Operation
|
83
|
-
include
|
83
|
+
include Model
|
84
84
|
model Song
|
85
85
|
|
86
86
|
def process(params)
|
@@ -89,7 +89,7 @@ class CrudTest < MiniTest::Spec
|
|
89
89
|
end
|
90
90
|
|
91
91
|
# uses :create as default if not set via ::action.
|
92
|
-
it { DefaultCreateOperation
|
92
|
+
it { DefaultCreateOperation.({}).model.must_equal Song.new }
|
93
93
|
|
94
94
|
# model Song, :action
|
95
95
|
class ModelUpdateOperation < CreateOperation
|
@@ -99,7 +99,7 @@ class CrudTest < MiniTest::Spec
|
|
99
99
|
# allows ::model, :action.
|
100
100
|
it do
|
101
101
|
Song.find_result = song = Song.new
|
102
|
-
ModelUpdateOperation
|
102
|
+
ModelUpdateOperation.({id: 1, song: {title: "Mercy Day For Mr. Vengeance"}}).model.must_equal song
|
103
103
|
end
|
104
104
|
|
105
105
|
|
@@ -111,13 +111,13 @@ class CrudTest < MiniTest::Spec
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
-
it { SetupModelOperation
|
114
|
+
it { SetupModelOperation.(song: {title: "Emily Kane"}).model.params.must_equal "{:song=>{:title=>\"Emily Kane\"}}" }
|
115
115
|
|
116
116
|
|
117
117
|
|
118
118
|
# no call to ::model raises error.
|
119
119
|
class NoModelOperation < Trailblazer::Operation
|
120
|
-
include
|
120
|
+
include Model
|
121
121
|
|
122
122
|
def process(params)
|
123
123
|
self
|
@@ -125,29 +125,11 @@ class CrudTest < MiniTest::Spec
|
|
125
125
|
end
|
126
126
|
|
127
127
|
# uses :create as default if not set via ::action.
|
128
|
-
it { assert_raises(RuntimeError){ NoModelOperation
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
# contract infers model_name.
|
133
|
-
# TODO: this a Rails/ActiveModel-specific test.
|
134
|
-
class ContractKnowsModelNameOperation < Trailblazer::Operation
|
135
|
-
include CRUD
|
136
|
-
model Song
|
137
|
-
include CRUD::ActiveModel
|
138
|
-
|
139
|
-
contract do
|
140
|
-
include Reform::Form::ActiveModel # this usually happens in Reform::Form::Rails.
|
141
|
-
property :title
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
it { ContractKnowsModelNameOperation.present(song: {title: "Direct Hit"}).contract.class.model_name.to_s.must_equal "CrudTest::Song" }
|
146
|
-
|
128
|
+
it { assert_raises(RuntimeError){ NoModelOperation.({}) } }
|
147
129
|
|
148
130
|
# allow passing validate(params, model, contract_class)
|
149
131
|
class OperationWithPrivateContract < Trailblazer::Operation
|
150
|
-
include
|
132
|
+
include Model
|
151
133
|
model Song
|
152
134
|
|
153
135
|
class Contract < Reform::Form
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class OperationBuilderTest < MiniTest::Spec
|
4
|
+
class ParentOperation < Trailblazer::Operation
|
5
|
+
def process(params)
|
6
|
+
end
|
7
|
+
|
8
|
+
class Sub < self
|
9
|
+
end
|
10
|
+
|
11
|
+
builds do |params|
|
12
|
+
Sub if params[:sub]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it { ParentOperation.run({}).last.class.must_equal ParentOperation }
|
17
|
+
it { ParentOperation.run({sub: true}).last.class.must_equal ParentOperation::Sub }
|
18
|
+
it { ParentOperation.({}).class.must_equal ParentOperation }
|
19
|
+
it { ParentOperation.({sub: true}).class.must_equal ParentOperation::Sub }
|
20
|
+
end
|
21
|
+
|
22
|
+
class OperationBuilderClassTest < MiniTest::Spec
|
23
|
+
class SuperOperation < Trailblazer::Operation
|
24
|
+
builds do |params|
|
25
|
+
self::Sub if params[:sub] # Sub is defined in ParentOperation.
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ParentOperation < Trailblazer::Operation
|
30
|
+
def process(params)
|
31
|
+
end
|
32
|
+
|
33
|
+
class Sub < self
|
34
|
+
end
|
35
|
+
|
36
|
+
self.builder_class = SuperOperation.builder_class
|
37
|
+
end
|
38
|
+
|
39
|
+
it { ParentOperation.({}).class.must_equal ParentOperation }
|
40
|
+
it { ParentOperation.({sub: true}).class.must_equal ParentOperation::Sub }
|
41
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "trailblazer/operation/dispatch"
|
3
|
+
|
4
|
+
|
5
|
+
class DslCallbackTest < MiniTest::Spec
|
6
|
+
module SongProcess
|
7
|
+
def process(params)
|
8
|
+
contract(OpenStruct.new).validate(params)
|
9
|
+
dispatch!
|
10
|
+
end
|
11
|
+
|
12
|
+
def _invocations
|
13
|
+
@_invocations ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(includer)
|
17
|
+
includer.contract do
|
18
|
+
property :title
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "inheritance across operations" do
|
24
|
+
class Operation < Trailblazer::Operation
|
25
|
+
include Dispatch
|
26
|
+
include SongProcess
|
27
|
+
|
28
|
+
callback do
|
29
|
+
on_change :default!
|
30
|
+
end
|
31
|
+
|
32
|
+
class Admin < self
|
33
|
+
callback do
|
34
|
+
on_change :admin_default!
|
35
|
+
end
|
36
|
+
|
37
|
+
callback(:after_save) { on_change :after_save! }
|
38
|
+
|
39
|
+
def admin_default!(*); _invocations << :admin_default!; end
|
40
|
+
def after_save!(*); _invocations << :after_save!; end
|
41
|
+
|
42
|
+
def process(*)
|
43
|
+
super
|
44
|
+
dispatch!(:after_save)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def default!(*); _invocations << :default!; end
|
49
|
+
end
|
50
|
+
|
51
|
+
it { Operation.({"title"=> "Love-less"})._invocations.must_equal([:default!]) }
|
52
|
+
it { Operation::Admin.({"title"=> "Love-less"})._invocations.must_equal([:default!, :admin_default!, :after_save!]) }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "Op.callback" do
|
56
|
+
it { Operation.callback(:default).must_equal Operation.callbacks[:default] }
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "Op.callback :after_save, AfterSaveCallback" do
|
60
|
+
class AfterSaveCallback < Disposable::Callback::Group
|
61
|
+
on_change :after_save!
|
62
|
+
end
|
63
|
+
|
64
|
+
class OpWithExternalCallback < Trailblazer::Operation
|
65
|
+
include Dispatch
|
66
|
+
include SongProcess
|
67
|
+
callback :after_save, AfterSaveCallback
|
68
|
+
|
69
|
+
def process(params)
|
70
|
+
contract(OpenStruct.new).validate(params)
|
71
|
+
dispatch!(:after_save)
|
72
|
+
end
|
73
|
+
|
74
|
+
def after_save!(*); _invocations << :after_save!; end
|
75
|
+
end
|
76
|
+
|
77
|
+
it { OpWithExternalCallback.("title"=>"Thunder Rising")._invocations.must_equal([:after_save!]) }
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "Op.callback :after_save, AfterSaveCallback do .. end" do
|
81
|
+
class DefaultCallback < Disposable::Callback::Group
|
82
|
+
on_change :default!
|
83
|
+
end
|
84
|
+
|
85
|
+
class OpUsingCallback < Trailblazer::Operation
|
86
|
+
include Dispatch
|
87
|
+
include SongProcess
|
88
|
+
callback :default, DefaultCallback
|
89
|
+
def default!(*); _invocations << :default!; end
|
90
|
+
end
|
91
|
+
|
92
|
+
class OpExtendingCallback < Trailblazer::Operation
|
93
|
+
include Dispatch
|
94
|
+
include SongProcess
|
95
|
+
callback :default, DefaultCallback do
|
96
|
+
on_change :after_save!
|
97
|
+
end
|
98
|
+
|
99
|
+
def default!(*); _invocations << :default!; end
|
100
|
+
def after_save!(*); _invocations << :after_save!; end
|
101
|
+
end
|
102
|
+
|
103
|
+
# this operation copies DefaultCallback and shouldn't run #after_save!.
|
104
|
+
it { OpUsingCallback.(title: "Thunder Rising")._invocations.must_equal([:default!]) }
|
105
|
+
# this operation copies DefaultCallback, extends it and runs #after_save!.
|
106
|
+
it { OpExtendingCallback.(title: "Thunder Rising")._invocations.must_equal([:default!, :after_save!]) }
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
# ::contract builds Reform::Form class
|
4
|
+
class DslContractTest < MiniTest::Spec
|
5
|
+
module SongProcess
|
6
|
+
def process(params)
|
7
|
+
validate(params, @model = OpenStruct.new)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "inheritance across operations" do
|
12
|
+
# inheritance
|
13
|
+
class Operation < Trailblazer::Operation
|
14
|
+
contract do
|
15
|
+
property :title
|
16
|
+
property :band
|
17
|
+
end
|
18
|
+
|
19
|
+
class JSON < self
|
20
|
+
contract do # inherit Contract
|
21
|
+
property :genre, validates: {presence: true}
|
22
|
+
property :band, virtual: true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# inherits subclassed Contract.
|
28
|
+
it { Operation.contract_class.wont_equal Operation::JSON.contract_class }
|
29
|
+
|
30
|
+
it do
|
31
|
+
form = Operation.contract_class.new(OpenStruct.new)
|
32
|
+
form.validate({})#.must_equal true
|
33
|
+
form.errors.to_s.must_equal "{}"
|
34
|
+
|
35
|
+
form = Operation::JSON.contract_class.new(OpenStruct.new)
|
36
|
+
form.validate({})#.must_equal true
|
37
|
+
form.errors.to_s.must_equal "{:genre=>[\"can't be blank\"]}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# allows overriding options
|
41
|
+
it do
|
42
|
+
form = Operation::JSON.contract_class.new(song = OpenStruct.new)
|
43
|
+
form.validate({genre: "Punkrock", band: "Osker"}).must_equal true
|
44
|
+
form.sync
|
45
|
+
|
46
|
+
song.genre.must_equal "Punkrock"
|
47
|
+
song.band.must_equal nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "Op.contract" do
|
52
|
+
it { Operation.contract.must_equal Operation.contract_class }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "Op.contract CommentForm" do
|
56
|
+
class SongForm < Reform::Form
|
57
|
+
property :songTitle, validates: {presence: true}
|
58
|
+
end
|
59
|
+
|
60
|
+
class OpWithExternalContract < Trailblazer::Operation
|
61
|
+
contract SongForm
|
62
|
+
include SongProcess
|
63
|
+
end
|
64
|
+
|
65
|
+
it { OpWithExternalContract.("songTitle"=> "Monsterparty").contract.songTitle.must_equal "Monsterparty" }
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "Op.contract CommentForm do .. end" do
|
69
|
+
class DifferentSongForm < Reform::Form
|
70
|
+
property :songTitle, validates: {presence: true}
|
71
|
+
end
|
72
|
+
|
73
|
+
class OpNotExtendingContract < Trailblazer::Operation
|
74
|
+
contract DifferentSongForm
|
75
|
+
include SongProcess
|
76
|
+
end
|
77
|
+
|
78
|
+
class OpExtendingContract < Trailblazer::Operation
|
79
|
+
contract DifferentSongForm do
|
80
|
+
property :genre
|
81
|
+
end
|
82
|
+
include SongProcess
|
83
|
+
end
|
84
|
+
|
85
|
+
# this operation copies DifferentSongForm and shouldn't have `genre`.
|
86
|
+
it do
|
87
|
+
contract = OpNotExtendingContract.("songTitle"=>"Monsterparty", "genre"=>"Punk").contract
|
88
|
+
contract.songTitle.must_equal "Monsterparty"
|
89
|
+
assert_raises(NoMethodError) { contract.genre }
|
90
|
+
end
|
91
|
+
|
92
|
+
# this operation copies DifferentSongForm and extends it with the property `genre`.
|
93
|
+
it do
|
94
|
+
contract = OpExtendingContract.("songTitle"=>"Monsterparty", "genre"=>"Punk").contract
|
95
|
+
contract.songTitle.must_equal "Monsterparty"
|
96
|
+
contract.genre.must_equal "Punk"
|
97
|
+
end
|
98
|
+
|
99
|
+
# of course, the original contract wasn't modified, either.
|
100
|
+
it do
|
101
|
+
assert_raises(NoMethodError) { DifferentSongForm.new(OpenStruct.new).genre }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|