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,36 @@
1
+ module Intermodal
2
+ module RSpec
3
+ module RequestValidations
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # This macro expects the following to be defined:
8
+ # let(:valid_payload) { request_payload that would succeed }
9
+ #
10
+ # You then pass a override hash and the expected response code,
11
+ # usually a 400 Bad Request or 422 Unprocessible Entity
12
+ #
13
+ # In overrides, if you pass a callable object such as a lambda or a proc,
14
+ # then it will be called at test time before merging into the request payload.
15
+ #
16
+ # Example:
17
+ #
18
+ # overrides: { apple: -> { tree.apples.sample } }
19
+ def expect_request_invalid(message, opts={}, &additional_examples)
20
+ _status = opts[:status] || 422
21
+ _overrides = opts[:overrides] or raise 'Must pass overrides: parameter'
22
+
23
+ context "when #{message}" do
24
+ let(:request_payload) { attributes.merge(overrides) }
25
+ let(:overrides) { Hash[_overrides.map(&eval_hash)] }
26
+ let(:eval_hash) { ->(x) { [x[0].to_s, maybe_call.(x[1]) ] } }
27
+ let(:maybe_call) { ->(x) { x.respond_to?(:call) ? x.call : x } }
28
+
29
+ expects_status _status
30
+ instance_eval(&additional_examples) if additional_examples
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,275 @@
1
+ module Intermodal
2
+ module RSpec
3
+ module Resources
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Intermodal::RSpec::Rack
8
+ include Intermodal::RSpec::AuthenticatedRequests
9
+ include Intermodal::RSpec::PaginatedCollection
10
+
11
+ let(:api) { raise "Must define let(:api)" }
12
+
13
+ let(:resource_collection_name) { resource_name.pluralize }
14
+ let(:model) { resource_name.camelize.constantize }
15
+ let(:parent_models) { parent_names.map { |p| p.to_s.singularize.camelize.constantize } }
16
+
17
+ let(:model_collection) do
18
+ 3.times { model.make!(model_factory_options) }
19
+ (model_parent ? model.by_parent(model_parent) : model.all )
20
+ end
21
+
22
+ let(:model_resource) { model.make!(model_factory_options) }
23
+ let(:model_factory_options) { (model_parent ? { parent_names.last.to_s.singularize => model_parent } : {} ) }
24
+ let(:model_parents) { parent_models.map { |p| p.make! } }
25
+ let(:model_parent) { model_parents.last }
26
+ let(:presenter) { api.presenter_for model }
27
+
28
+ let(:collection) { model_collection }
29
+ let(:paginated_collection) { model_collection.paginate(:page => page, :per_page => per_page) }
30
+ let(:presented_collection) { parser.decode(paginated_collection.send("to_#{format}", :presenter => presenter, :root => collection_element_name, :scope => presenter_scope_for_index)) }
31
+ let(:page) { 1 }
32
+ let(:per_page) { api.default_per_page }
33
+
34
+ let(:resource_element_name) { model.name.demodulize.underscore }
35
+ let(:collection_element_name) { resource_element_name.pluralize }
36
+ let(:expected_resource) { model.find(model_resource.id) }
37
+ let(:persisted_resource_id) { body['id'] }
38
+ let(:resource_after_create) { model.find(persisted_resource_id) }
39
+ let(:resource_after_update) { model.find(resource_id) }
40
+ let(:resource_after_destroy) { resource_after_update }
41
+ let(:resource) { parser.decode(expected_resource.send("to_#{format}", :presenter => presenter, :scope => presenter_scope)) }
42
+ let(:presenter_scope) { nil }
43
+ let(:presenter_scope_for_index) { presenter_scope }
44
+ let(:resource_id) { resource['id'] }
45
+ let(:parent_ids) { parent_names.zip(model_parents.map { |m| m.id }) }
46
+
47
+ let(:namespace) { nil }
48
+ let(:namespace_path) { ( namespace ? "/#{namespace}" : nil ) }
49
+ let(:parent_path) { parent_ids.map { |p, p_id| "/#{p}/#{p_id}" }.join }
50
+ let(:collection_url) { [ namespace_path, parent_path, "/#{resource_collection_name}.#{format}" ].join }
51
+ let(:resource_url) { [ namespace_path, parent_path, "/#{resource_collection_name}/#{resource_id}.#{format}" ].join }
52
+
53
+ let(:request_json_payload) { request_payload.to_json }
54
+ let(:request_xml_payload) { request_payload.to_xml(:root => resource_element_name) }
55
+
56
+ let(:malformed_json_payload) { '{ "bad": [ "data": ] }' }
57
+ let(:malformed_xml_payload) { '<bad><data></bad></data>' }
58
+
59
+ # DELETE specs expect record to be deleted.
60
+ # Override this if you are using something such as ActiveResource:
61
+ # let(:record_not_found_error) { ActiveResource::ResourceNotFound }
62
+ let(:record_not_found_error) { ActiveRecord::RecordNotFound }
63
+
64
+ # Some of the test examples assume that the database is blank.
65
+ # For example, index tests require injecting 3 resources and expects
66
+ # the index endpoint to return exactly 3 resources.
67
+ let(:reset_datastore!) { } # Do nothing by default
68
+ end
69
+
70
+ module ClassMethods
71
+ def given_create_attributes(values = {})
72
+ let(:valid_create_attributes) { values }
73
+ end
74
+
75
+ def given_update_attributes(values = {})
76
+ let(:valid_update_attributes) { values }
77
+ end
78
+
79
+ def metadata_for_resources(resource_name, options = {})
80
+ if resource_name.is_a?(Array)
81
+ parents = resource_name
82
+ resource_name = parents.pop
83
+ end
84
+ resource_name = resource_name.to_s if resource_name.is_a?(Symbol)
85
+
86
+ { :formats => [ :json ],
87
+ :resource_name => resource_name,
88
+ :model_name => resource_name.singularize,
89
+ :encoding => 'utf-8',
90
+ :parents => parents || [],
91
+ :namespace_path => ( options[:namespace] ? "/#{options[:namespace]}" : nil )
92
+ }.merge(options)
93
+ end
94
+
95
+ def metadata_for_formatted_resource(format, options = {})
96
+ { :format => format,
97
+ :collection_url => collection_url_for_resource(options[:namespace_path], options[:parents], options[:resource_name], format),
98
+ :resource_url => resource_url_for_resource(options[:namespace_path], options[:parents], options[:resource_name], format),
99
+ :mime_type => Mime::Type.lookup_by_extension(format).to_s
100
+ }.merge(options)
101
+ end
102
+
103
+ def parent_path_for_resource(parents)
104
+ parents.map { |p| "/#{p}/:id" }.join
105
+ end
106
+
107
+ def collection_url_for_resource(namespace, parents, resource_name, format)
108
+ [ namespace, parent_path_for_resource(parents), "/#{resource_name}.#{format}" ].join
109
+ end
110
+
111
+ def resource_url_for_resource(namespace, parents, resource_name, format)
112
+ [ namespace, parent_path_for_resource(parents), "/#{resource_name}/:id.#{format}" ].join
113
+ end
114
+
115
+ def resources(resource_name, options = {}, &blk)
116
+ options = metadata_for_resources(resource_name, options)
117
+ _resource_name = options[:resource_name].singularize
118
+
119
+ # If you want xml, pass it as formats: [ :json, :xml ]
120
+ options[:formats].each do |format|
121
+ format_options = metadata_for_formatted_resource(format, options)
122
+ context format_options[:collection_url], format_options do
123
+ let(:namespace) { options[:namespace] }
124
+ let(:resource_name) { _resource_name }
125
+ let(:parent_names) { options[:parents] }
126
+ let(:format) { format }
127
+ let(options[:parents].last) { model_parent } if options[:parents].last
128
+ instance_eval(&blk) if blk
129
+ end
130
+ end
131
+ end
132
+
133
+ def expects_resource_crud(options = {}, &additional_examples)
134
+ expected_actions = options[:only] || [ :index, :show, :create, :update, :destroy ]
135
+ expected_actions -= options[:except] if options[:except]
136
+
137
+ expected_actions.each do |action|
138
+ send("expects_#{action}")
139
+ end
140
+
141
+ instance_eval(&additional_examples) if additional_examples
142
+ end
143
+
144
+ STANDARD_SUCCESSFUL_STATUS_FOR = {
145
+ :index => 200,
146
+ :show => 200,
147
+ :create => 201,
148
+ :update => 200,
149
+ :destroy => 204 }
150
+
151
+ STANDARD_REQUEST_FOR = {
152
+ :index => { :method => :get, :end_point => :collection_url },
153
+ :show => { :method => :get, :end_point => :resource_url },
154
+ :create => { :method => :post, :end_point => :collection_url, :payload => proc do valid_create_attributes end },
155
+ :update => { :method => :put, :end_point => :resource_url, :payload => proc do valid_update_attributes end },
156
+ :destroy => { :method => :delete, :end_point => :resource_url } }
157
+
158
+ def request_resource_action(action, options = {}, &blk)
159
+ options = {
160
+ :mime_type => metadata[:mime_type],
161
+ :encoding => metadata[:encoding],
162
+ :status => STANDARD_SUCCESSFUL_STATUS_FOR[action],
163
+ :collection_url => metadata[:collection_url],
164
+ :resource_url => metadata[:resource_url]
165
+ }.merge(STANDARD_REQUEST_FOR[action]).merge(options)
166
+
167
+ request options[:method], options[options[:end_point]], options[:payload] do
168
+ let(:request_url) { send(options[:end_point]) }
169
+ expects_status(options[:status])
170
+ expects_content_type(options[:mime_type], options[:encoding])
171
+ instance_eval(&blk) if blk
172
+ end
173
+ end
174
+
175
+ def expects_index(options = {}, &additional_examples)
176
+ request_resource_action(:index, options) do
177
+ unless options[:skip_presentation_test]
178
+ it "should return a list of all #{metadata[:resource_name]}" do
179
+ reset_datastore!
180
+ collection.should_not be_empty
181
+ body.should eql(presented_collection)
182
+ end
183
+ end
184
+
185
+ expects_unauthorized_access_to_respond_with_401
186
+ unless options[:skip_pagination_examples]
187
+ expects_paginated_resource do
188
+ before(:each) { reset_datastore! }
189
+ end
190
+ end
191
+ instance_eval(&additional_examples) if additional_examples
192
+ end
193
+ end
194
+
195
+ def expects_show(options = {}, &additional_examples)
196
+ request_resource_action(:show, options) do
197
+ it "should return a #{metadata[:resource_name]} of id 1" do
198
+ resource.should_not be_nil
199
+ body.should eql(resource)
200
+ end
201
+
202
+ with_non_existent_resource_should_respond_with_404
203
+ expects_unauthorized_access_to_respond_with_401
204
+ instance_eval(&additional_examples) if additional_examples
205
+ end
206
+ end
207
+
208
+ def expects_create(options = {}, &additional_examples)
209
+ request_resource_action(:create, options) do
210
+ it "should return the newly created #{metadata[:resource_name]}" do
211
+ body.should eql(parser.decode(resource_after_create.send("to_#{format}", { :presenter => presenter, :scope => presenter_scope})))
212
+ end
213
+
214
+ with_malformed_data_should_respond_with_400
215
+ expects_unauthorized_access_to_respond_with_401
216
+ instance_eval(&additional_examples) if additional_examples
217
+ end
218
+ end
219
+
220
+ def expects_update(options = {}, &additional_examples)
221
+ request_resource_action(:update, options) do
222
+ it "should update #{metadata[:resource_name]}" do
223
+ response.should_not be(nil)
224
+ valid_update_attributes.each do |updated_attribute, updated_value|
225
+ resource_after_update[updated_attribute].should eql(updated_value)
226
+ end
227
+ end
228
+
229
+ with_malformed_data_should_respond_with_400
230
+ with_non_existent_resource_should_respond_with_404
231
+ expects_unauthorized_access_to_respond_with_401
232
+ instance_eval(&additional_examples) if additional_examples
233
+ end
234
+ end
235
+
236
+ def expects_destroy(options = {}, &additional_examples)
237
+ request_resource_action(:destroy, {mime_type: nil, encoding: nil}.merge(options)) do
238
+ it "should delete #{metadata[:resource_name]}" do
239
+ response.should_not be(nil)
240
+ lambda { resource_after_destroy }.should raise_error(record_not_found_error)
241
+ end
242
+
243
+ with_non_existent_resource_should_respond_with_404
244
+ expects_unauthorized_access_to_respond_with_401
245
+ instance_eval(&additional_examples) if additional_examples
246
+ end
247
+ end
248
+
249
+ def with_malformed_data_should_respond_with_400
250
+ context "with malformed #{metadata[:format]} payload" do
251
+ let(:request_raw_payload) { send("malformed_#{format}_payload") }
252
+
253
+ expects_status(400)
254
+ expects_content_type(metadata[:mime_type], metadata[:encoding])
255
+ end
256
+ end
257
+
258
+ def with_non_existent_resource_should_respond_with_404
259
+ context 'with non-existent resource' do
260
+ let(:resource_id) { 0 } # Assumes that persisted datastore never uses id of 0
261
+ expects_status 404
262
+ end
263
+ end
264
+
265
+ def expects_json_presentation(_presenter_scope = nil)
266
+ let(:presenter_scope) { _presenter_scope } if _presenter_scope
267
+
268
+ it 'should present a JSON object' do
269
+ body.should eql(presented_resource)
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,51 @@
1
+ module Intermodal
2
+ module RSpec
3
+ module HTTP
4
+ RFC2616_HTTP_STATUS_CODES =
5
+ [[100, "Continue"],
6
+ [101, "Switching Protocols"],
7
+ [200, "OK"],
8
+ [201, "Created"],
9
+ [202, "Accepted"],
10
+ [203, "Non-Authoritative Information"],
11
+ [204, "No Content"],
12
+ [205, "Reset Content"],
13
+ [206, "Partial Content"],
14
+ [300, "Multiple Choices"],
15
+ [301, "Moved Permanently"],
16
+ [302, "Found"],
17
+ [303, "See Other"],
18
+ [304, "Not Modified"],
19
+ [305, "Use Proxy"],
20
+ [306, "(Unused)"],
21
+ [307, "Temporary Redirect"],
22
+ [400, "Bad Request"],
23
+ [401, "Unauthorized"],
24
+ [402, "Payment Required"],
25
+ [403, "Forbidden"],
26
+ [404, "Not Found"],
27
+ [405, "Method Not Allowed"],
28
+ [406, "Not Acceptable"],
29
+ [407, "Proxy Authentication Required"],
30
+ [408, "Request Timeout"],
31
+ [409, "Conflict"],
32
+ [410, "Gone"],
33
+ [411, "Length Required"],
34
+ [412, "Precondition Failed"],
35
+ [413, "Request Entity Too Large"],
36
+ [414, "Request-URI Too Long"],
37
+ [415, "Unsupported Media Type"],
38
+ [416, "Requested Range Not Satisfiable"],
39
+ [417, "Expectation Failed"],
40
+ [422, "Unprocessable Entity"],
41
+ [500, "Internal Server Error"],
42
+ [501, "Not Implemented"],
43
+ [502, "Bad Gateway"],
44
+ [503, "Service Unavailable"],
45
+ [504, "Gateway Timeout"],
46
+ [505, "HTTP Version Not Supported"]]
47
+
48
+ STATUS_CODES = RFC2616_HTTP_STATUS_CODES.inject({}) { |h, e| h[e[0].to_s]=e[1]; h }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,86 @@
1
+ require 'active_support/concern'
2
+
3
+ module Intermodal
4
+ module RSpec
5
+ module Validators
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Make sure to define:
10
+ # let(:resource) # resource to be tested
11
+ # let(:associated_resource) # association and resource both owned by same account
12
+ # let(:invalid_associated_resource) # association and resource owned by different account
13
+ #
14
+ # Optional
15
+ # let(:attribute) # Attribute being validated
16
+ # let(:message) # Error message
17
+ #
18
+ def self.validates_same_account(association, &additional_examples)
19
+ context "validates #{association} is owned by same account" do
20
+ let(:attribute) { association }
21
+ let(:message) { 'must belong to the same account' }
22
+
23
+ context 'when association belongs to same account' do
24
+ it 'should be_valid' do
25
+ expect(resource.account_id).to eql(associated_resource.account_id)
26
+ expect(resource).to be_valid
27
+ expect(resource.errors).not_to have_key(attribute)
28
+ end
29
+ end
30
+
31
+ context 'when association belongs to a different account' do
32
+ before(:each) { resource.send("#{association}=", invalid_associated_resource) }
33
+
34
+ it 'should not be valid and report the error' do
35
+ expect(resource.account_id).not_to eql(invalid_associated_resource.account_id)
36
+ expect(resource).not_to be_valid
37
+ expect(resource.errors).to have_key(attribute)
38
+ expect(resource.errors[attribute]).to include(message)
39
+ end
40
+ end
41
+
42
+ instance_eval(&additional_examples) if additional_examples
43
+ end
44
+ end
45
+
46
+ # Make sure to define:
47
+ # let(:resource) # resource to be tested
48
+ # let(:associated_resource) # association and resource owned by different accounts
49
+ # let(:invalid_associated_resource) # association and resource both owned by same account
50
+ #
51
+ # Optional
52
+ # let(:attribute) # Attribute being validated
53
+ # let(:message) # Error message
54
+ #
55
+ def self.validates_different_account(association, &additional_examples)
56
+ context "validates #{association} is owned by a different account" do
57
+ let(:attribute) { association }
58
+ let(:message) { 'must belong to a different account' }
59
+
60
+ context 'when association belongs to same account' do
61
+ before(:each) { resource.send("#{association}=", invalid_associated_resource) }
62
+
63
+ it 'should not be valid' do
64
+ expect(resource.account_id).to eql(invalid_associated_resource.account_id)
65
+ expect(resource).not_to be_valid
66
+ expect(resource.errors).to have_key(attribute)
67
+ expect(resource.errors[attribute]).to include(message)
68
+ end
69
+ end
70
+
71
+ context 'when association belongs to a different account' do
72
+
73
+ it 'should not be valid and report the error' do
74
+ expect(resource.account_id).not_to eql(associated_resource.account_id)
75
+ expect(resource).to be_valid
76
+ expect(resource.errors).not_to have_key(attribute)
77
+ end
78
+ end
79
+
80
+ instance_eval(&additional_examples) if additional_examples
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end