intermodal 0.0.1 → 0.4.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 (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +1 -1
  5. data/README +6 -4
  6. data/Rakefile +6 -0
  7. data/intermodal.gemspec +51 -0
  8. data/lib/generators/intermodal_generator.rb +8 -0
  9. data/lib/intermodal.rb +122 -0
  10. data/lib/intermodal/api.rb +246 -0
  11. data/lib/intermodal/api/configuration.rb +40 -0
  12. data/lib/intermodal/api/railties.rb +21 -0
  13. data/lib/intermodal/concerns/acceptors/named_resource.rb +12 -0
  14. data/lib/intermodal/concerns/acceptors/resource.rb +12 -0
  15. data/lib/intermodal/concerns/controllers/accountability.rb +17 -0
  16. data/lib/intermodal/concerns/controllers/anonymous.rb +24 -0
  17. data/lib/intermodal/concerns/controllers/authenticatable.rb +24 -0
  18. data/lib/intermodal/concerns/controllers/paginated_collection.rb +25 -0
  19. data/lib/intermodal/concerns/controllers/presentation.rb +17 -0
  20. data/lib/intermodal/concerns/controllers/resource.rb +51 -0
  21. data/lib/intermodal/concerns/controllers/resource_linking.rb +58 -0
  22. data/lib/intermodal/concerns/let.rb +21 -0
  23. data/lib/intermodal/concerns/models/access_credential.rb +28 -0
  24. data/lib/intermodal/concerns/models/account.rb +16 -0
  25. data/lib/intermodal/concerns/models/accountability.rb +47 -0
  26. data/lib/intermodal/concerns/models/db_access_token.rb +29 -0
  27. data/lib/intermodal/concerns/models/has_parent_resource.rb +45 -0
  28. data/lib/intermodal/concerns/models/presentation.rb +25 -0
  29. data/lib/intermodal/concerns/models/redis_access_token.rb +60 -0
  30. data/lib/intermodal/concerns/models/resource_linking.rb +126 -0
  31. data/lib/intermodal/concerns/models/sanitize_html.rb +35 -0
  32. data/lib/intermodal/concerns/presenters/named_resource.rb +12 -0
  33. data/lib/intermodal/concerns/presenters/resource.rb +14 -0
  34. data/lib/intermodal/concerns/rails/rails_3_stack.rb +42 -0
  35. data/lib/intermodal/concerns/rails/rails_4_stack.rb +17 -0
  36. data/lib/intermodal/concerns/rails/use_warden.rb +21 -0
  37. data/lib/intermodal/config.rb +15 -0
  38. data/lib/intermodal/configuration.rb +11 -0
  39. data/lib/intermodal/controllers/api_controller.rb +26 -0
  40. data/lib/intermodal/controllers/linking_resource_controller.rb +8 -0
  41. data/lib/intermodal/controllers/nested_resource_controller.rb +18 -0
  42. data/lib/intermodal/controllers/resource_controller.rb +11 -0
  43. data/lib/intermodal/dsl/controllers.rb +125 -0
  44. data/lib/intermodal/dsl/mapping.rb +79 -0
  45. data/lib/intermodal/dsl/presentation_helpers.rb +107 -0
  46. data/lib/intermodal/mapping/acceptor.rb +2 -2
  47. data/lib/intermodal/mapping/mapper.rb +39 -13
  48. data/lib/intermodal/mapping/presenter.rb +12 -6
  49. data/lib/intermodal/proxies/linking_resources.rb +58 -0
  50. data/lib/intermodal/proxies/will_paginate.rb +85 -0
  51. data/lib/intermodal/rack/auth.rb +29 -0
  52. data/lib/intermodal/rack/dummy_store.rb +24 -0
  53. data/lib/intermodal/rack/rescue.rb +82 -0
  54. data/lib/intermodal/responders/linking_resource_responder.rb +21 -0
  55. data/lib/intermodal/responders/resource_responder.rb +64 -0
  56. data/lib/intermodal/rspec/acceptors.rb +79 -0
  57. data/lib/intermodal/rspec/models/accountability.rb +114 -0
  58. data/lib/intermodal/rspec/models/has_parent_resource.rb +132 -0
  59. data/lib/intermodal/rspec/models/resource_linking.rb +234 -0
  60. data/lib/intermodal/rspec/models/sanitization.rb +84 -0
  61. data/lib/intermodal/rspec/presenters.rb +92 -0
  62. data/lib/intermodal/rspec/requests/authenticated_requests.rb +17 -0
  63. data/lib/intermodal/rspec/requests/linked_resources.rb +180 -0
  64. data/lib/intermodal/rspec/requests/paginated_collection.rb +60 -0
  65. data/lib/intermodal/rspec/requests/rack.rb +142 -0
  66. data/lib/intermodal/rspec/requests/request_validations.rb +36 -0
  67. data/lib/intermodal/rspec/requests/resources.rb +275 -0
  68. data/lib/intermodal/rspec/requests/rfc2616_status_codes.rb +51 -0
  69. data/lib/intermodal/rspec/validators.rb +86 -0
  70. data/lib/intermodal/validators/account_validator.rb +27 -0
  71. data/lib/intermodal/validators/different_account_validator.rb +27 -0
  72. data/lib/intermodal/version.rb +3 -0
  73. data/spec/mapping/acceptors_spec.rb +142 -0
  74. data/spec/mapping/presenters_spec.rb +186 -0
  75. data/spec/models/accountability_spec.rb +13 -0
  76. data/spec/models/has_parent_resource_spec.rb +18 -0
  77. data/spec/models/resource_linking_spec.rb +21 -0
  78. data/spec/proxies/will_paginate_spec.rb +163 -0
  79. data/spec/rack/auth_spec.rb +51 -0
  80. data/spec/requests/linked_resources.rb +37 -0
  81. data/spec/requests/nested_resources_spec.rb +54 -0
  82. data/spec/requests/resources_spec.rb +50 -0
  83. data/spec/spec_helper.rb +53 -0
  84. data/spec/support/api.rb +50 -0
  85. data/spec/support/app/class_builder.rb +41 -0
  86. data/spec/support/app/db/adapter_helper.rb +53 -0
  87. data/spec/support/app/db/authentication_schema_helper.rb +62 -0
  88. data/spec/support/app/db/migration_helper.rb +44 -0
  89. data/spec/support/app/schema.rb +101 -0
  90. data/spec/support/application.rb +23 -0
  91. data/spec/support/blueprints.rb +41 -0
  92. data/spec/support/epiphyte.rb +29 -0
  93. metadata +393 -52
  94. data/lib/intermodal/base.rb +0 -13
  95. data/lib/intermodal/declare_controllers.rb +0 -102
  96. data/lib/intermodal/mapping.rb +0 -4
  97. data/lib/intermodal/mapping/dsl.rb +0 -76
@@ -0,0 +1,114 @@
1
+ module Intermodal
2
+ module RSpec
3
+ # All Intermodal controllers require models that belongs to an
4
+ # account object and responds to #get. This allows the controllers
5
+ # to fetch records authorized by the account.
6
+ #
7
+ # This RSpec macro tests these assumptions.
8
+ #
9
+ # Usage:
10
+ #
11
+ # class Resource < ActiveRecord::Base
12
+ # include Intermodal::Models::Accountability
13
+ # end
14
+ #
15
+ # describe Resource do
16
+ # include Intermodal::RSpec::Accountability
17
+ #
18
+ # concerned_with_accountability
19
+ # end
20
+ #
21
+ # It works with Remarkable. It might work with Shoulda
22
+ #
23
+ # If you don't want to use either, you can try
24
+ #
25
+ # describe Resource do
26
+ # include Intermodal::RSpec::Accountability
27
+ #
28
+ # implements_get_interface
29
+ # end
30
+ #
31
+ module Accountability
32
+ extend ActiveSupport::Concern
33
+
34
+ module ClassMethods
35
+ def concerned_with_accountability(&blk)
36
+ instance_eval(&blk) if blk
37
+
38
+ context 'when concerned with accountability' do
39
+ let(:model) { subject.class }
40
+ it { should belong_to :account }
41
+ it { should validate_presence_of :account_id }
42
+ it { model.should respond_to :by_account_id }
43
+ it { model.should respond_to :by_account }
44
+
45
+ implements_get_interface
46
+ end
47
+ end
48
+
49
+ def implements_get_interface(&blk)
50
+ describe '.get' do
51
+ let(:model) { subject.class }
52
+ let(:different_account) { Account.make! }
53
+
54
+ it { model.should respond_to :get }
55
+
56
+ context ':all' do
57
+ context 'when unscoped to account' do
58
+ let(:collection) { model.get(:all) }
59
+ it 'should find all resources' do
60
+ collection.should include(resource)
61
+ collection.size.should eql(1)
62
+ end
63
+ end
64
+
65
+ context 'when scoped to account' do
66
+ let(:collection) { model.get(:all, account: account) }
67
+ it 'should find all resources' do
68
+ collection.should include(resource)
69
+ collection.size.should eql(1)
70
+ end
71
+ end
72
+ end
73
+
74
+ context 'by id' do
75
+ it 'should find resource by id' do
76
+ model.get(resource.id).should eql(resource)
77
+ end
78
+
79
+ it 'should return a writeable resource' do
80
+ model.get(resource.id).should_not be_readonly
81
+ end
82
+ end
83
+
84
+ context 'by account' do
85
+ it 'should find resource scoped to account' do
86
+ model.get(resource.id, account: account).should eql(resource)
87
+ end
88
+
89
+ it 'should find resource scoped to account id' do
90
+ model.get(resource.id, account_id: account.id).should eql(resource)
91
+ end
92
+
93
+ it 'should find a writeable resource scoped to account id' do
94
+ model.get(resource.id, account_id: account.id).should_not be_readonly
95
+ end
96
+
97
+ it 'should not find resource scoped to a different account' do
98
+ expect { model.get(resource.id, account: different_account) }.to raise_error(ActiveRecord::RecordNotFound)
99
+ end
100
+
101
+ it 'should not find resource scoped to a different account id ' do
102
+ expect { model.get(resource.id, account_id: different_account.id) }.to raise_error(ActiveRecord::RecordNotFound)
103
+ end
104
+ end
105
+
106
+ instance_eval(&blk) if blk
107
+ end
108
+ end # implements_get_interface
109
+ end # ClassMethods
110
+
111
+ end
112
+ end
113
+ end
114
+
@@ -0,0 +1,132 @@
1
+ module Intermodal
2
+ module RSpec
3
+ # All Intermodal nested resource controllers require models that
4
+ # belongs to an parent object that belongs to an account. It should
5
+ # also respond to #get and verify authorization for the account.
6
+ #
7
+ # This RSpec macro tests these assumptions.
8
+ #
9
+ # Usage:
10
+ #
11
+ # class ParentResource < ActiveRecord::Base
12
+ # include Intermodal::Models::Accountability
13
+ # end
14
+ #
15
+ # class NestedResource < ActiveRecord::Base
16
+ # include Intermodal::Models::HasParentResources
17
+ #
18
+ # parent_resource :parent_resource
19
+ # end
20
+ #
21
+ # describe NestedResource do
22
+ # include Intermodal::RSpec::HasParentResource
23
+ #
24
+ # concerned_with_parent_resource :parent_resource
25
+ # end
26
+ #
27
+ # It works with Remarkable. It might work with Shoulda
28
+ #
29
+ # If you don't want to use either, you can pass:
30
+ #
31
+ # describe NestedResource do
32
+ # include Intermodal::RSpec::HasParentResource
33
+ #
34
+ # let(:parent_resource_name) { :parent_resource }
35
+ # let(:different_parent) { ParentResource.make!(:account => account) }
36
+ # let(:parent_with_different_account) { ParentResource.make!(:account => different_account) }
37
+ # let(:different_account) { Account.make! }
38
+ #
39
+ # implements_get_interface_for_nested_resource
40
+ # end
41
+ #
42
+ module HasParentResource
43
+ extend ActiveSupport::Concern
44
+
45
+ included do
46
+ include Intermodal::RSpec::Accountability
47
+ end
48
+
49
+ module ClassMethods
50
+
51
+ # NOTE: I am cheating. It assumes if the class method is defined then
52
+ # it is a scope
53
+ def should_respond_to_scope(scope_method)
54
+ it "should have scope #{scope_method}" do
55
+ subject.class.should respond_to(scope_method)
56
+ end
57
+ end
58
+
59
+ def concerned_with_parent_resource(_parent_resource_name, options = {}, &blk)
60
+ extra_get_examples = options[:extra_get_examples]
61
+
62
+ context "when concerned with parent resource #{_parent_resource_name}" do
63
+ let(:parent_resource_name) { _parent_resource_name }
64
+ let(:parent_id_name) { "#{parent_resource_name}_id" }
65
+ let(:parent_model) { parent_resource_name.to_s.camelize.constantize }
66
+ let(:different_parent) { parent_model.make! }
67
+ let(:parent_with_different_account) { parent_model.make!(:account => different_account) }
68
+
69
+ instance_eval(&blk) if blk
70
+
71
+ it { should belong_to _parent_resource_name } unless options[:skip_association_examples]
72
+ it { should validate_presence_of _parent_resource_name } unless options[:skip_validation_examples]
73
+
74
+ [ :by_parent_id, :by_parent, "by_#{_parent_resource_name}_id", "by_#{_parent_resource_name}" ].each do |scope|
75
+ should_respond_to_scope(scope)
76
+ end
77
+
78
+ implements_get_interface_for_nested_resource(&extra_get_examples)
79
+ end
80
+ end
81
+
82
+ def implements_get_interface_for_nested_resource(&blk)
83
+ implements_get_interface do
84
+
85
+ context 'by parent' do
86
+ it 'should find resource scoped to parent' do
87
+ model.get(subject.id, :parent => subject.send(parent_resource_name)).should eql(subject)
88
+ end
89
+
90
+ it 'should find resource scoped to parent id' do
91
+ model.get(subject.id, :parent_id => subject.send(parent_resource_name).id).should eql(subject)
92
+ end
93
+
94
+ it 'should find writeable resource scoped to parent id' do
95
+ model.get(subject.id, :parent_id => subject.send(parent_resource_name).id).should_not be_readonly
96
+ end
97
+ end
98
+
99
+ context 'by parent and account' do
100
+ it 'should find resource scoped to account id and parent id' do
101
+ model.get(subject.id, :parent_id => subject.send(parent_resource_name).id, :account_id => account.id).should eql(subject)
102
+ end
103
+
104
+ it 'should find writeable resource scoped to account id and parent id' do
105
+ model.get(subject.id, :parent_id => subject.send(parent_resource_name).id, :account_id => account.id).should_not be_readonly
106
+ end
107
+
108
+ it 'should not find resource scoped to a different parent' do
109
+ lambda { model.get(subject.id, :parent => different_parent) }.should raise_error(ActiveRecord::RecordNotFound)
110
+ end
111
+
112
+ it 'should not find resource scoped to a different parent id' do
113
+ lambda { model.get(subject.id, :parent_id => different_parent.id) }.should raise_error(ActiveRecord::RecordNotFound)
114
+ end
115
+
116
+ it 'should not find resource scoped to a parent in a different account' do
117
+ lambda { model.get(subject.id, :parent_id => parent_with_different_account.id, :account_id => account) }.should raise_error(ActiveRecord::RecordNotFound)
118
+ end
119
+
120
+ it 'should not find resource scoped to a correct parent but incorrect account' do
121
+ lambda { model.get(subject.id, :parent_id => subject.send(parent_resource_name).id, :account_id => different_account) }.should raise_error(ActiveRecord::RecordNotFound)
122
+ end
123
+ end
124
+
125
+ instance_eval(&blk) if blk
126
+ end
127
+ end # implements_get_interface_for_nested_resource
128
+ end
129
+ end
130
+ end
131
+ end
132
+
@@ -0,0 +1,234 @@
1
+ module Intermodal
2
+ module RSpec
3
+ module ResourceLinking
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Intermodal::RSpec::HasParentResource
8
+ end
9
+
10
+ module ClassMethods
11
+ # To use this, you must define the resources in SpecHelpers::Application
12
+ # This ensures that all the linked resources are scoped to the same account
13
+ #
14
+ # Example:
15
+ #
16
+ # concerned_with_resource_linking :item, :vendor
17
+ #
18
+ # Somewhere within the spec scope, you must define the following:
19
+ #
20
+ # let(:model) { Sku }
21
+ # let(:skus) { item.vendors << vendors }
22
+ # let(:item) { Item.make(:account => account) }
23
+ # let(:vendors) { (1..3).map { Vendor.make(:account => account) }
24
+ # let(:account) { Account.make }
25
+ #
26
+ # Optional:
27
+ # If you are not running Remarkable or Shoulda, you can pass :skip_association_examples => true
28
+ def concerned_with_resource_linking(_parent_resource_name, _target_resource_name, options = {}, &blk)
29
+ context "when concerned with linking #{_parent_resource_name} to #{_target_resource_name}", options do
30
+ _metadata = metadata
31
+
32
+ let(:parent_resource_name) { _parent_resource_name }
33
+ let(:target_resource_name) { _target_resource_name }
34
+
35
+ let(:model_name) { model.model_name.demodulize.underscore.to_sym }
36
+ let(:model_association_name) { model_name.to_s.pluralize }
37
+ let(:parent) { send(parent_resource_name) }
38
+ let(:parent_with_different_account) { parent.class.make(:account => different_account) }
39
+ let(:targets) { send(target_resource_name.to_s.pluralize) }
40
+ let(:list) { send(model_association_name) }
41
+
42
+ let(:target_association_name) { target_resource_name.to_s.pluralize }
43
+ let(:target_model) { (_metadata[:class_name] || target_resource_name).to_s.camelize.constantize }
44
+ let(:target_model_blueprint) { proc do target_model.make!(:account => account) end }
45
+ let(:target_model_blueprint_with_different_account) { proc do target_model.make!(:account => different_account) end }
46
+ let(:target_accounts) { targets.map(&:account) }
47
+ let(:target_with_different_account) { target_model_blueprint_with_different_account.call() }
48
+ let(:targets_with_different_account) { (1..3).map { target_model_blueprint_with_different_account.call() } }
49
+ let(:target_ids_with_different_account) { targets_with_different_account.map(&:id) }
50
+
51
+ instance_eval(&blk) if blk
52
+
53
+ # Examples
54
+ get_examples_for_linked_targets = proc do
55
+ pending 'by target' do
56
+ it 'should find targets scoped to the same account' do
57
+ target_accounts.each do |target_account|
58
+ target_account.should eql(parent_account)
59
+ end
60
+ model.get(subject.id, :account => parent_account).should eql(subject)
61
+ end
62
+ it 'should not find targets scoped to another account' do
63
+ parent.send(target_association_name) << target_with_different_account
64
+ model.get(:all, :account => different_account).send("to_#{target_resource}_ids").should_not include(target_with_different_account.id)
65
+ end
66
+ let(:parent_association) {
67
+ parent.send(target_association_name) << target_with_different_account
68
+ parent.send(target_association_name) }
69
+ let(:query) { model.get(:all, :account => different_account).to_sql }
70
+ #debug_examples :target_with_different_account, :parent_association, :account, :different_account, :parent, :query
71
+ end
72
+ end
73
+
74
+ concerned_with_parent_resource(_parent_resource_name, { :extra_get_examples => get_examples_for_linked_targets }.merge(options)) do
75
+ let(:parent_model) { parent.class }
76
+ let(:parent_account) { parent.account }
77
+ instance_eval(&options[:extra_parent_resource_examples]) if options[:extra_parent_resource_examples]
78
+ end
79
+
80
+ unless options[:skip_association_examples]
81
+ it { should belong_to target_resource_name }
82
+ it { subject.class.should respond_to :by_target_id }
83
+ it { subject.class.should respond_to "by_#{target_resource_name}_id" }
84
+ end
85
+
86
+ describe ".to_#{_target_resource_name}_ids" do
87
+ subject { list; parent.send(model_association_name).send("to_#{_target_resource_name}_ids") }
88
+
89
+ it "should return a list of #{_target_resource_name} ids" do
90
+ should be_kind_of(Array)
91
+ subject.map do |id|
92
+ (id + 0).should eql(id)
93
+ end
94
+ end
95
+
96
+ it "should return all #{_target_resource_name} ids that linked to the #{_parent_resource_name}" do
97
+ target_model.where(:id => subject).each do |linked_resource|
98
+ targets.should include(linked_resource)
99
+ end
100
+ end
101
+ end
102
+
103
+ describe '.replace' do
104
+ let(:replacement_targets) { (1..3).map { target_model_blueprint.call() } }
105
+ let(:replacement_target_ids) { replacement_targets.map(&:id) }
106
+ let(:original_target_ids) { list; parent.send(target_association_name).map(&:id) }
107
+ let(:updated_target_ids) { model.get(:all, :parent => parent).to_target_ids }
108
+ subject { list; model.replace(parent.id, replacement_target_ids) }
109
+
110
+ it "should link the target ids to the parent #{_parent_resource_name}" do
111
+ original_target_ids.should_not be_empty
112
+ subject.should_not be_empty
113
+ updated_target_ids.sort.should eql(replacement_target_ids.sort)
114
+ end
115
+
116
+ context 'when scoped to an account' do
117
+ it 'should link to a parent scoped to the account' do
118
+ model.replace(parent, replacement_target_ids, :account => account).should_not be_empty
119
+ end
120
+
121
+ it 'should not link to a parent scoped to a different account' do
122
+ lambda { model.replace(parent_with_different_account.id, replacement_target_ids, :account => account) }.should raise_error(ActiveRecord::RecordNotFound)
123
+ end
124
+
125
+ it "should only accept target ids that belong to the same account" do
126
+ subject.should_not be_empty
127
+ model.replace(parent.id, target_ids_with_different_account, :account => account).should be_empty
128
+ end
129
+
130
+ it 'should only accept target ids that belong to the same account id' do
131
+ subject.should_not be_empty
132
+ model.replace(parent.id, target_ids_with_different_account, :account_id => account.id).should be_empty
133
+ end
134
+ end
135
+
136
+ it "should delete the existing links to the parent #{_parent_resource_name}" do
137
+ original_target_ids.should_not be_empty
138
+ subject.should_not be_empty
139
+ (original_target_ids & updated_target_ids).should be_empty
140
+ end
141
+ end
142
+
143
+ describe '.append' do
144
+ let(:additional_targets) { (1..3).map { target_model_blueprint.call() } }
145
+ let(:additional_target_ids) { additional_targets.map(&:id) }
146
+ let(:original_target_ids) { list; parent.send(target_association_name).map(&:id) }
147
+ let(:updated_target_ids) { parent.send(target_association_name).map(&:id) }
148
+ subject { list; model.append(parent.id, additional_target_ids) }
149
+
150
+ it "should link additional #{_target_resource_name} ids to the parent #{_parent_resource_name}" do
151
+ subject.should_not be_empty
152
+ additional_target_ids do |additional_target_id|
153
+ updated_target_ids.should include(additional_target_id)
154
+ end
155
+ end
156
+
157
+ context 'when scoped to an account' do
158
+ it 'should link to a parent scoped to the account' do
159
+ model.append(parent, additional_target_ids, :account => account).should_not be_empty
160
+ end
161
+
162
+ it 'should not link to a parent scoped to a different account' do
163
+ lambda { model.append(parent_with_different_account.id, additional_target_ids, :account => account) }.should raise_error(ActiveRecord::RecordNotFound)
164
+ end
165
+
166
+ it "should only accept target ids that belong to the same account" do
167
+ subject.should_not be_empty
168
+ model.append(parent.id, target_ids_with_different_account, :account => account).should_not be_empty
169
+ updated_target_ids.should eql(updated_target_ids - target_ids_with_different_account)
170
+ end
171
+
172
+ it 'should only accept target ids that belong to the same account id' do
173
+ subject.should_not be_empty
174
+ model.append(parent.id, target_ids_with_different_account, :account_id => account.id).should_not be_empty
175
+ updated_target_ids.should eql(updated_target_ids - target_ids_with_different_account)
176
+ end
177
+ end
178
+
179
+ it "should keep existing links to the parent #{_parent_resource_name}" do
180
+ original_target_ids.should_not be_empty
181
+ subject.should_not be_empty
182
+ original_target_ids do |original_target_id|
183
+ updated_target_ids.should include(original_target_id)
184
+ end
185
+ end
186
+
187
+ it "should not append existing links to the parent #{_parent_resource_name}" do
188
+ 2.times { model.append(parent.id, additional_target_ids) }
189
+ additional_target_ids.each do |additional_target_id|
190
+ model.by_parent(parent).by_target_id(additional_target_id).length.should eql(1)
191
+ end
192
+ end
193
+ end
194
+
195
+ describe '.remove' do
196
+ let(:original_target_ids) { list; parent.send(target_association_name).map(&:id) }
197
+ let(:removed_target_ids) { original_target_ids[0..1] }
198
+ let(:remaining_target_ids) { original_target_ids - removed_target_ids }
199
+ let(:updated_target_ids) { parent.send(target_association_name).map(&:id) }
200
+ subject { list; model.remove(parent.id, removed_target_ids) }
201
+
202
+ it "should delete the existing links to the parent #{_parent_resource_name}" do
203
+ removed_target_ids.should_not be_empty
204
+ subject.should_not be_empty
205
+ removed_target_ids do |removed_target_id|
206
+ updated_target_ids.should_not include(removed_target_id)
207
+ end
208
+ end
209
+
210
+ context 'when scoped to an account' do
211
+ it 'should link to a parent scoped to the account' do
212
+ model.remove(parent, removed_target_ids, :account => account).should_not be_empty
213
+ end
214
+
215
+ it 'should not unlink from a parent scoped to a different account' do
216
+ lambda { model.remove(parent_with_different_account.id, removed_target_ids, :account => account) }.should raise_error(ActiveRecord::RecordNotFound)
217
+ end
218
+ end
219
+
220
+ it "should keep remaining links to the parent" do
221
+ removed_target_ids.should_not be_empty
222
+ subject.should_not be_empty
223
+ remaining_target_ids do |original_target_id|
224
+ updated_target_ids.should include(remaining_target_id)
225
+ end
226
+ end
227
+
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end