intermodal 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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