jsonapi-resources 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +274 -102
  3. data/jsonapi-resources.gemspec +1 -0
  4. data/lib/jsonapi-resources.rb +15 -0
  5. data/lib/jsonapi/active_record_operations_processor.rb +21 -10
  6. data/lib/jsonapi/acts_as_resource_controller.rb +175 -0
  7. data/lib/jsonapi/configuration.rb +11 -0
  8. data/lib/jsonapi/error_codes.rb +7 -4
  9. data/lib/jsonapi/exceptions.rb +23 -15
  10. data/lib/jsonapi/formatter.rb +5 -5
  11. data/lib/jsonapi/include_directives.rb +67 -0
  12. data/lib/jsonapi/operation.rb +185 -65
  13. data/lib/jsonapi/operation_result.rb +38 -5
  14. data/lib/jsonapi/operation_results.rb +33 -0
  15. data/lib/jsonapi/operations_processor.rb +49 -9
  16. data/lib/jsonapi/paginator.rb +31 -17
  17. data/lib/jsonapi/request.rb +347 -163
  18. data/lib/jsonapi/resource.rb +159 -56
  19. data/lib/jsonapi/resource_controller.rb +1 -234
  20. data/lib/jsonapi/resource_serializer.rb +55 -69
  21. data/lib/jsonapi/resources/version.rb +1 -1
  22. data/lib/jsonapi/response_document.rb +87 -0
  23. data/lib/jsonapi/routing_ext.rb +17 -11
  24. data/test/controllers/controller_test.rb +602 -326
  25. data/test/fixtures/active_record.rb +96 -6
  26. data/test/fixtures/line_items.yml +7 -1
  27. data/test/fixtures/numeros_telefone.yml +3 -0
  28. data/test/fixtures/purchase_orders.yml +6 -0
  29. data/test/integration/requests/request_test.rb +129 -60
  30. data/test/integration/routes/routes_test.rb +17 -17
  31. data/test/test_helper.rb +23 -5
  32. data/test/unit/jsonapi_request/jsonapi_request_test.rb +48 -0
  33. data/test/unit/operation/operations_processor_test.rb +242 -54
  34. data/test/unit/resource/resource_test.rb +108 -2
  35. data/test/unit/serializer/include_directives_test.rb +108 -0
  36. data/test/unit/serializer/response_document_test.rb +61 -0
  37. data/test/unit/serializer/serializer_test.rb +679 -520
  38. metadata +26 -2
@@ -18,6 +18,7 @@ module JSONAPI
18
18
 
19
19
  @fields = options.fetch(:fields, {})
20
20
  @include = options.fetch(:include, [])
21
+ @include_directives = options[:include_directives]
21
22
  @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
22
23
  @route_formatter = options.fetch(:route_formatter, JSONAPI.configuration.route_formatter)
23
24
  @base_url = options.fetch(:base_url, '')
@@ -28,10 +29,9 @@ module JSONAPI
28
29
  is_resource_collection = source.respond_to?(:to_ary)
29
30
 
30
31
  @included_objects = {}
32
+ @include_directives ||= JSONAPI::IncludeDirectives.new(@include)
31
33
 
32
- requested_associations = parse_includes(@include)
33
-
34
- process_primary(source, requested_associations)
34
+ process_primary(source, @include_directives.include_directives)
35
35
 
36
36
  included_objects = []
37
37
  primary_objects = []
@@ -47,11 +47,7 @@ module JSONAPI
47
47
 
48
48
  primary_hash = {data: is_resource_collection ? primary_objects : primary_objects[0]}
49
49
 
50
- if included_objects.size > 0
51
- primary_hash[:included] = included_objects
52
- else
53
- primary_hash
54
- end
50
+ primary_hash[:included] = included_objects if included_objects.size > 0
55
51
  primary_hash
56
52
  end
57
53
 
@@ -72,34 +68,11 @@ module JSONAPI
72
68
  end
73
69
 
74
70
  private
75
- # Convert an array of associated objects to include along with the primary document in the form of
76
- # ['comments','author','comments.tags','author.posts'] into a structure that tells what we need to include
77
- # from each association.
78
- def parse_includes(includes)
79
- requested_associations = {}
80
- includes.each do |include|
81
- include = include.to_s.underscore
82
-
83
- pos = include.index('.')
84
- if pos
85
- association_name = include[0, pos].to_sym
86
- requested_associations[association_name] ||= {}
87
- requested_associations[association_name].store(:include_children, true)
88
- requested_associations[association_name].store(:include_related, parse_includes([include[pos+1, include.length]]))
89
- else
90
- association_name = include.to_sym
91
- requested_associations[association_name] ||= {}
92
- requested_associations[association_name].store(:include, true)
93
- end
94
- end if includes.is_a?(Array)
95
- return requested_associations
96
- end
97
-
98
71
  # Process the primary source object(s). This will then serialize associated object recursively based on the
99
72
  # requested includes. Fields are controlled fields option for each resource type, such
100
73
  # as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
101
74
  # The fields options controls both fields and included links references.
102
- def process_primary(source, requested_associations)
75
+ def process_primary(source, include_directives)
103
76
  if source.respond_to?(:to_ary)
104
77
  source.each do |resource|
105
78
  id = resource.id
@@ -107,25 +80,37 @@ module JSONAPI
107
80
  set_primary(@primary_class_name, id)
108
81
  end
109
82
 
110
- add_included_object(@primary_class_name, id, object_hash(resource, requested_associations), true)
83
+ add_included_object(@primary_class_name, id, object_hash(resource, include_directives), true)
111
84
  end
112
85
  else
113
86
  return {} if source.nil?
114
87
 
115
88
  resource = source
116
89
  id = resource.id
117
- add_included_object(@primary_class_name, id, object_hash(source, requested_associations), true)
90
+ add_included_object(@primary_class_name, id, object_hash(source, include_directives), true)
118
91
  end
119
92
  end
120
93
 
121
94
  # Returns a serialized hash for the source model
122
- def object_hash(source, requested_associations)
123
- obj_hash = attribute_hash(source)
124
- links = links_hash(source, requested_associations)
95
+ def object_hash(source, include_directives)
96
+ obj_hash = {}
97
+
98
+ id_format = source.class._attribute_options(:id)[:format]
99
+ #protect against ids that were declared as an attribute, but did not have a format set.
100
+ id_format = 'id' if id_format == :default
101
+ obj_hash['id'] = format_value(source.id, id_format)
125
102
 
126
103
  obj_hash['type'] = format_key(source.class._type.to_s)
127
- obj_hash['id'] ||= format_value(source.id, :id, source)
128
- obj_hash.merge!({links: links}) unless links.empty?
104
+
105
+ links = relationship_links(source)
106
+ obj_hash['links'] = links unless links.empty?
107
+
108
+ attributes = attribute_hash(source)
109
+ obj_hash['attributes'] = attributes unless attributes.empty?
110
+
111
+ relationships = relationship_data(source, include_directives)
112
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
113
+
129
114
  return obj_hash
130
115
  end
131
116
 
@@ -142,20 +127,13 @@ module JSONAPI
142
127
 
143
128
  fields.each_with_object({}) do |name, hash|
144
129
  format = source.class._attribute_options(name)[:format]
145
- if format == :default && name == :id
146
- format = 'id'
130
+ unless name == :id
131
+ hash[format_key(name)] = format_value(source.send(name), format)
147
132
  end
148
- hash[format_key(name)] = format_value(
149
- source.send(name),
150
- format,
151
- source
152
- )
153
133
  end
154
134
  end
155
135
 
156
- # Returns a hash of links for the requested associations for a resource, filtered by the resource
157
- # class's fetchable method
158
- def links_hash(source, requested_associations)
136
+ def relationship_data(source, include_directives)
159
137
  associations = source.class._associations
160
138
  requested = requested_fields(source.class._type)
161
139
  fields = associations.keys
@@ -167,15 +145,14 @@ module JSONAPI
167
145
 
168
146
  included_associations = source.fetchable_fields & associations.keys
169
147
 
170
- links = {}
171
- links[:self] = self_href(source)
148
+ data = {}
172
149
 
173
- associations.each_with_object(links) do |(name, association), hash|
150
+ associations.each_with_object(data) do |(name, association), hash|
174
151
  if included_associations.include? name
175
- ia = requested_associations.is_a?(Hash) ? requested_associations[name] : nil
152
+ ia = include_directives[:include_related][name]
176
153
 
177
154
  include_linkage = ia && ia[:include]
178
- include_linked_children = ia && ia[:include_children]
155
+ include_linked_children = ia && !ia[:include_related].empty?
179
156
 
180
157
  if field_set.include?(name)
181
158
  hash[format_key(name)] = link_object(source, association, include_linkage)
@@ -193,9 +170,9 @@ module JSONAPI
193
170
  id = resource.id
194
171
  associations_only = already_serialized?(type, id)
195
172
  if include_linkage && !associations_only
196
- add_included_object(type, id, object_hash(resource, ia[:include_related]))
173
+ add_included_object(type, id, object_hash(resource, ia))
197
174
  elsif include_linked_children || associations_only
198
- links_hash(resource, ia[:include_related])
175
+ relationship_data(resource, ia)
199
176
  end
200
177
  end
201
178
  elsif association.is_a?(JSONAPI::Association::HasMany)
@@ -204,9 +181,9 @@ module JSONAPI
204
181
  id = resource.id
205
182
  associations_only = already_serialized?(type, id)
206
183
  if include_linkage && !associations_only
207
- add_included_object(type, id, object_hash(resource, ia[:include_related]))
184
+ add_included_object(type, id, object_hash(resource, ia))
208
185
  elsif include_linked_children || associations_only
209
- links_hash(resource, ia[:include_related])
186
+ relationship_data(resource, ia)
210
187
  end
211
188
  end
212
189
  end
@@ -215,6 +192,13 @@ module JSONAPI
215
192
  end
216
193
  end
217
194
 
195
+ def relationship_links(source)
196
+ links = {}
197
+ links[:self] = self_href(source)
198
+
199
+ links
200
+ end
201
+
218
202
  def formatted_module_path(source)
219
203
  source.class.name =~ /::[^:]+\Z/ ? (@route_formatter.format($`).freeze.gsub('::', '/') + '/').downcase : ''
220
204
  end
@@ -233,7 +217,7 @@ module JSONAPI
233
217
  end
234
218
 
235
219
  def self_link(source, association)
236
- "#{self_href(source)}/links/#{format_route(association.name)}"
220
+ "#{self_href(source)}/relationships/#{format_route(association.name)}"
237
221
  end
238
222
 
239
223
  def related_link(source, association)
@@ -263,17 +247,19 @@ module JSONAPI
263
247
 
264
248
  def link_object_has_one(source, association)
265
249
  link_object_hash = {}
266
- link_object_hash[:self] = self_link(source, association)
267
- link_object_hash[:related] = related_link(source, association)
268
- link_object_hash[:linkage] = has_one_linkage(source, association)
250
+ link_object_hash[:links] = {}
251
+ link_object_hash[:links][:self] = self_link(source, association)
252
+ link_object_hash[:links][:related] = related_link(source, association)
253
+ link_object_hash[:data] = has_one_linkage(source, association)
269
254
  link_object_hash
270
255
  end
271
256
 
272
257
  def link_object_has_many(source, association, include_linkage)
273
258
  link_object_hash = {}
274
- link_object_hash[:self] = self_link(source, association)
275
- link_object_hash[:related] = related_link(source, association)
276
- link_object_hash[:linkage] = has_many_linkage(source, association) if include_linkage
259
+ link_object_hash[:links] = {}
260
+ link_object_hash[:links][:self] = self_link(source, association)
261
+ link_object_hash[:links][:related] = related_link(source, association)
262
+ link_object_hash[:data] = has_many_linkage(source, association) if include_linkage
277
263
  link_object_hash
278
264
  end
279
265
 
@@ -291,9 +277,9 @@ module JSONAPI
291
277
  value = source.send(foreign_key)
292
278
 
293
279
  if association.is_a?(JSONAPI::Association::HasMany)
294
- value.map { |value| IdValueFormatter.format(value, {}) }
280
+ value.map { |value| IdValueFormatter.format(value) }
295
281
  elsif association.is_a?(JSONAPI::Association::HasOne)
296
- IdValueFormatter.format(value, {})
282
+ IdValueFormatter.format(value)
297
283
  end
298
284
  end
299
285
 
@@ -324,9 +310,9 @@ module JSONAPI
324
310
  @key_formatter.format(key)
325
311
  end
326
312
 
327
- def format_value(value, format, source)
313
+ def format_value(value, format)
328
314
  value_formatter = JSONAPI::ValueFormatter.value_formatter_for(format)
329
- value_formatter.format(value, source)
315
+ value_formatter.format(value)
330
316
  end
331
317
  end
332
318
  end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = "0.3.3"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
@@ -0,0 +1,87 @@
1
+ module JSONAPI
2
+ class ResponseDocument
3
+ def initialize(operation_results, options = {})
4
+ @operation_results = operation_results
5
+ @options = options
6
+
7
+ @key_formatter = @options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
8
+ end
9
+
10
+ def contents
11
+ hash = results_to_hash
12
+
13
+ meta = top_level_meta
14
+ hash.merge!(meta: meta) unless meta.empty?
15
+
16
+ hash
17
+ end
18
+
19
+ def status
20
+ if @operation_results.has_errors?
21
+ @operation_results.all_errors[0].status
22
+ else
23
+ @operation_results.results[0].code
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def serializer
30
+ @serializer ||= @options.fetch(:resource_serializer_klass, JSONAPI::ResourceSerializer).new(
31
+ @options.fetch(:primary_resource_klass),
32
+ include_directives: @options[:include_directives],
33
+ fields: @options[:fields],
34
+ base_url: @options.fetch(:base_url, ''),
35
+ key_formatter: @key_formatter,
36
+ route_formatter: @options.fetch(:route_formatter, JSONAPI.configuration.route_formatter)
37
+ )
38
+ end
39
+
40
+ def top_level_meta
41
+ meta = @options.fetch(:base_meta, {})
42
+
43
+ meta.merge!(@operation_results.meta)
44
+
45
+ @operation_results.results.each do |result|
46
+ meta.merge!(result.meta)
47
+ end
48
+
49
+ meta.deep_transform_keys { |key| @key_formatter.format(key) }
50
+ end
51
+
52
+ def results_to_hash
53
+ if @operation_results.has_errors?
54
+ {errors: @operation_results.all_errors}
55
+ else
56
+ if @operation_results.results.length == 1
57
+ result = @operation_results.results[0]
58
+
59
+ case result
60
+ when JSONAPI::ResourceOperationResult
61
+ serializer.serialize_to_hash(result.resource)
62
+ when JSONAPI::ResourcesOperationResult
63
+ serializer.serialize_to_hash(result.resources)
64
+ when JSONAPI::LinksObjectOperationResult
65
+ serializer.serialize_to_links_hash(result.parent_resource,
66
+ result.association)
67
+ when JSONAPI::OperationResult
68
+ {}
69
+ end
70
+
71
+ elsif @operation_results.results.length > 1
72
+ resources = []
73
+ @operation_results.results.each do |result|
74
+ case result
75
+ when JSONAPI::ResourceOperationResult
76
+ resources.push(result.resource)
77
+ when JSONAPI::ResourcesOperationResult
78
+ resources.concat(result.resources)
79
+ end
80
+ end
81
+
82
+ serializer.serialize_to_hash(resources)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -58,8 +58,7 @@ module ActionDispatch
58
58
  options[:controller] ||= @resource_type
59
59
  options.merge!(res.routing_resource_options)
60
60
 
61
- # Route using the primary_key. Can be overridden using routing_resource_options
62
- options[:param] ||= res._primary_key
61
+ options[:param] = :id
63
62
 
64
63
  options[:path] = format_route(@resource_type)
65
64
 
@@ -91,21 +90,22 @@ module ActionDispatch
91
90
  options = links.extract_options!.dup
92
91
 
93
92
  res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
93
+ options[:controller] ||= res._type.to_s
94
94
 
95
95
  methods = links_methods(options)
96
96
 
97
97
  if methods.include?(:show)
98
- match "links/#{formatted_association_name}", controller: res._type.to_s,
98
+ match "relationships/#{formatted_association_name}", controller: options[:controller],
99
99
  action: 'show_association', association: link_type.to_s, via: [:get]
100
100
  end
101
101
 
102
102
  if methods.include?(:update)
103
- match "links/#{formatted_association_name}", controller: res._type.to_s,
103
+ match "relationships/#{formatted_association_name}", controller: options[:controller],
104
104
  action: 'update_association', association: link_type.to_s, via: [:put, :patch]
105
105
  end
106
106
 
107
107
  if methods.include?(:destroy)
108
- match "links/#{formatted_association_name}", controller: res._type.to_s,
108
+ match "relationships/#{formatted_association_name}", controller: options[:controller],
109
109
  action: 'destroy_association', association: link_type.to_s, via: [:delete]
110
110
  end
111
111
  end
@@ -116,54 +116,60 @@ module ActionDispatch
116
116
  options = links.extract_options!.dup
117
117
 
118
118
  res = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
119
+ options[:controller] ||= res._type.to_s
119
120
 
120
121
  methods = links_methods(options)
121
122
 
122
123
  if methods.include?(:show)
123
- match "links/#{formatted_association_name}", controller: res._type.to_s,
124
+ match "relationships/#{formatted_association_name}", controller: options[:controller],
124
125
  action: 'show_association', association: link_type.to_s, via: [:get]
125
126
  end
126
127
 
127
128
  if methods.include?(:create)
128
- match "links/#{formatted_association_name}", controller: res._type.to_s,
129
+ match "relationships/#{formatted_association_name}", controller: options[:controller],
129
130
  action: 'create_association', association: link_type.to_s, via: [:post]
130
131
  end
131
132
 
132
133
  if methods.include?(:update)
133
- match "links/#{formatted_association_name}", controller: res._type.to_s,
134
+ match "relationships/#{formatted_association_name}", controller: options[:controller],
134
135
  action: 'update_association', association: link_type.to_s, via: [:put, :patch]
135
136
  end
136
137
 
137
138
  if methods.include?(:destroy)
138
- match "links/#{formatted_association_name}/:keys", controller: res._type.to_s,
139
+ match "relationships/#{formatted_association_name}/:keys", controller: options[:controller],
139
140
  action: 'destroy_association', association: link_type.to_s, via: [:delete]
140
141
  end
141
142
  end
142
143
 
143
144
  def jsonapi_related_resource(*association)
144
145
  source = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
146
+ options = association.extract_options!.dup
145
147
 
146
148
  association_name = association.first
147
149
  association = source._associations[association_name]
148
150
 
149
151
  formatted_association_name = format_route(association.name)
150
152
  related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(association.class_name.underscore.pluralize))
153
+ options[:controller] ||= related_resource._type.to_s
151
154
 
152
- match "#{formatted_association_name}", controller: related_resource._type.to_s,
155
+
156
+ match "#{formatted_association_name}", controller: options[:controller],
153
157
  association: association.name, source: resource_type_with_module_prefix(source._type),
154
158
  action: 'get_related_resource', via: [:get]
155
159
  end
156
160
 
157
161
  def jsonapi_related_resources(*association)
158
162
  source = JSONAPI::Resource.resource_for(resource_type_with_module_prefix)
163
+ options = association.extract_options!.dup
159
164
 
160
165
  association_name = association.first
161
166
  association = source._associations[association_name]
162
167
 
163
168
  formatted_association_name = format_route(association.name)
164
169
  related_resource = JSONAPI::Resource.resource_for(resource_type_with_module_prefix(association.class_name.underscore))
170
+ options[:controller] ||= related_resource._type.to_s
165
171
 
166
- match "#{formatted_association_name}", controller: related_resource._type.to_s,
172
+ match "#{formatted_association_name}", controller: options[:controller],
167
173
  association: association.name, source: resource_type_with_module_prefix(source._type),
168
174
  action: 'get_related_resources', via: [:get]
169
175
  end
@@ -4,10 +4,6 @@ def set_content_type_header!
4
4
  @request.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
5
5
  end
6
6
 
7
- class ConfigControllerTest < ActionController::TestCase
8
-
9
- end
10
-
11
7
  class PostsControllerTest < ActionController::TestCase
12
8
  def test_index
13
9
  get :index
@@ -57,16 +53,32 @@ class PostsControllerTest < ActionController::TestCase
57
53
  assert_equal 1, json_response['included'].size
58
54
  end
59
55
 
56
+ def test_index_include_one_level_query_count
57
+ query_count = count_queries do
58
+ get :index, {include: 'author'}
59
+ end
60
+ assert_response :success
61
+ assert_equal 2, query_count
62
+ end
63
+
64
+ def test_index_include_two_levels_query_count
65
+ query_count = count_queries do
66
+ get :index, {include: 'author,author.comments'}
67
+ end
68
+ assert_response :success
69
+ assert_equal 3, query_count
70
+ end
71
+
60
72
  def test_index_filter_by_ids_and_fields
61
73
  get :index, {filter: {id: '1,2'}, fields: {posts: 'id,title,author'}}
62
74
  assert_response :success
63
75
  assert_equal 2, json_response['data'].size
64
76
 
65
- # type, id, title, links
66
- assert_equal 4, json_response['data'][0].size
77
+ # type, id, links, attributes, relationships
78
+ assert_equal 5, json_response['data'][0].size
67
79
  assert json_response['data'][0].has_key?('type')
68
80
  assert json_response['data'][0].has_key?('id')
69
- assert json_response['data'][0].has_key?('title')
81
+ assert json_response['data'][0]['attributes'].has_key?('title')
70
82
  assert json_response['data'][0].has_key?('links')
71
83
  end
72
84
 
@@ -75,11 +87,11 @@ class PostsControllerTest < ActionController::TestCase
75
87
  assert_response :success
76
88
  assert_equal 2, json_response['data'].size
77
89
 
78
- # type, id, title, links
79
- assert_equal 4, json_response['data'][0].size
90
+ # type, id, links, attributes, relationships
91
+ assert_equal 5, json_response['data'][0].size
80
92
  assert json_response['data'][0].has_key?('type')
81
93
  assert json_response['data'][0].has_key?('id')
82
- assert json_response['data'][0].has_key?('title')
94
+ assert json_response['data'][0]['attributes'].has_key?('title')
83
95
  assert json_response['data'][0].has_key?('links')
84
96
  end
85
97
 
@@ -94,11 +106,11 @@ class PostsControllerTest < ActionController::TestCase
94
106
  assert_response :success
95
107
  assert_equal 2, json_response['data'].size
96
108
 
97
- # links, id, type
98
- assert_equal 3, json_response['data'][0].size
109
+ # type, id, links, relationships
110
+ assert_equal 4, json_response['data'][0].size
99
111
  assert json_response['data'][0].has_key?('type')
100
112
  assert json_response['data'][0].has_key?('id')
101
- assert json_response['data'][0]['links'].has_key?('author')
113
+ assert json_response['data'][0]['relationships'].has_key?('author')
102
114
  end
103
115
 
104
116
  def test_filter_association_single
@@ -172,58 +184,35 @@ class PostsControllerTest < ActionController::TestCase
172
184
  end
173
185
 
174
186
  def test_sorting_asc
175
- get :index, {sort: '+title'}
176
-
177
- assert_response :success
178
- assert_equal "A First Post", json_response['data'][0]['title']
179
- end
180
-
181
- # Plus symbol may be replaced by a space
182
- def test_sorting_asc_with_space
183
- get :index, {sort: ' title'}
184
-
185
- assert_response :success
186
- assert_equal "A First Post", json_response['data'][0]['title']
187
- end
188
-
189
- # Plus symbol may be sent uriencoded ('%2b')
190
- def test_sorting_asc_with_encoded_plus
191
- get :index, {sort: '%2btitle'}
187
+ get :index, {sort: 'title'}
192
188
 
193
189
  assert_response :success
194
- assert_equal "A First Post", json_response['data'][0]['title']
190
+ assert_equal "A First Post", json_response['data'][0]['attributes']['title']
195
191
  end
196
192
 
197
193
  def test_sorting_desc
198
194
  get :index, {sort: '-title'}
199
195
 
200
196
  assert_response :success
201
- assert_equal "Update This Later - Multiple", json_response['data'][0]['title']
197
+ assert_equal "Update This Later - Multiple", json_response['data'][0]['attributes']['title']
202
198
  end
203
199
 
204
200
  def test_sorting_by_multiple_fields
205
- get :index, {sort: '+title,+body'}
201
+ get :index, {sort: 'title,body'}
206
202
 
207
203
  assert_response :success
208
204
  assert_equal '14', json_response['data'][0]['id']
209
205
  end
210
206
 
211
207
  def test_invalid_sort_param
212
- get :index, {sort: '+asdfg'}
208
+ get :index, {sort: 'asdfg'}
213
209
 
214
210
  assert_response :bad_request
215
211
  assert_match /asdfg is not a valid sort criteria for post/, response.body
216
212
  end
217
213
 
218
- def test_invalid_sort_param_missing_direction
219
- get :index, {sort: 'title'}
220
-
221
- assert_response :bad_request
222
- assert_match /title must start with a direction/, response.body
223
- end
224
-
225
214
  def test_excluded_sort_param
226
- get :index, {sort: '+id'}
215
+ get :index, {sort: 'id'}
227
216
 
228
217
  assert_response :bad_request
229
218
  assert_match /id is not a valid sort criteria for post/, response.body
@@ -233,8 +222,8 @@ class PostsControllerTest < ActionController::TestCase
233
222
  get :show, {id: '1'}
234
223
  assert_response :success
235
224
  assert json_response['data'].is_a?(Hash)
236
- assert_equal 'New post', json_response['data']['title']
237
- assert_equal 'A body!!!', json_response['data']['body']
225
+ assert_equal 'New post', json_response['data']['attributes']['title']
226
+ assert_equal 'A body!!!', json_response['data']['attributes']['body']
238
227
  assert_nil json_response['included']
239
228
  end
240
229
 
@@ -242,11 +231,11 @@ class PostsControllerTest < ActionController::TestCase
242
231
  get :show, {id: '1', include: 'comments'}
243
232
  assert_response :success
244
233
  assert json_response['data'].is_a?(Hash)
245
- assert_equal 'New post', json_response['data']['title']
246
- assert_equal 'A body!!!', json_response['data']['body']
247
- assert_nil json_response['data']['links']['tags']['linkage']
234
+ assert_equal 'New post', json_response['data']['attributes']['title']
235
+ assert_equal 'A body!!!', json_response['data']['attributes']['body']
236
+ assert_nil json_response['data']['relationships']['tags']['data']
248
237
  assert matches_array?([{'type' => 'comments', 'id' => '1'}, {'type' => 'comments', 'id' => '2'}],
249
- json_response['data']['links']['comments']['linkage'])
238
+ json_response['data']['relationships']['comments']['data'])
250
239
  assert_equal 2, json_response['included'].size
251
240
  end
252
241
 
@@ -254,9 +243,8 @@ class PostsControllerTest < ActionController::TestCase
254
243
  get :show, {id: '1', fields: {posts: 'author'}}
255
244
  assert_response :success
256
245
  assert json_response['data'].is_a?(Hash)
257
- assert_nil json_response['data']['title']
258
- assert_nil json_response['data']['body']
259
- assert_equal '1', json_response['data']['links']['author']['linkage']['id']
246
+ assert_nil json_response['data']['attributes']
247
+ assert_equal '1', json_response['data']['relationships']['author']['data']['id']
260
248
  end
261
249
 
262
250
  def test_show_single_with_fields_string
@@ -295,19 +283,21 @@ class PostsControllerTest < ActionController::TestCase
295
283
  {
296
284
  data: {
297
285
  type: 'posts',
298
- title: 'JR is Great',
299
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
300
- links: {
301
- author: {linkage: {type: 'people', id: '3'}}
286
+ attributes: {
287
+ title: 'JR is Great',
288
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
289
+ },
290
+ relationships: {
291
+ author: {data: {type: 'people', id: '3'}}
302
292
  }
303
293
  }
304
294
  }
305
295
 
306
296
  assert_response :created
307
297
  assert json_response['data'].is_a?(Hash)
308
- assert_equal '3', json_response['data']['links']['author']['linkage']['id']
309
- assert_equal 'JR is Great', json_response['data']['title']
310
- assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['body']
298
+ assert_equal '3', json_response['data']['relationships']['author']['data']['id']
299
+ assert_equal 'JR is Great', json_response['data']['attributes']['title']
300
+ assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['attributes']['body']
311
301
  end
312
302
 
313
303
  def test_create_link_to_missing_object
@@ -316,10 +306,12 @@ class PostsControllerTest < ActionController::TestCase
316
306
  {
317
307
  data: {
318
308
  type: 'posts',
319
- title: 'JR is Great',
320
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
321
- links: {
322
- author: {linkage: {type: 'people', id: '304567'}}
309
+ attributes: {
310
+ title: 'JR is Great',
311
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
312
+ },
313
+ relationships: {
314
+ author: {data: {type: 'people', id: '304567'}}
323
315
  }
324
316
  }
325
317
  }
@@ -335,11 +327,13 @@ class PostsControllerTest < ActionController::TestCase
335
327
  {
336
328
  data: {
337
329
  type: 'posts',
338
- asdfg: 'aaaa',
339
- title: 'JR is Great',
340
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
341
- links: {
342
- author: {linkage: {type: 'people', id: '3'}}
330
+ attributes: {
331
+ asdfg: 'aaaa',
332
+ title: 'JR is Great',
333
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
334
+ },
335
+ relationships: {
336
+ author: {data: {type: 'people', id: '3'}}
343
337
  }
344
338
  }
345
339
  }
@@ -354,9 +348,11 @@ class PostsControllerTest < ActionController::TestCase
354
348
  {
355
349
  data: {
356
350
  type: 'posts',
357
- title: 'JSONAPIResources is the greatest thing...',
358
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
359
- links: {
351
+ attributes: {
352
+ title: 'JSONAPIResources is the greatest thing...',
353
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
354
+ },
355
+ relationships: {
360
356
  author: nil
361
357
  }
362
358
  }
@@ -380,18 +376,22 @@ class PostsControllerTest < ActionController::TestCase
380
376
  data: [
381
377
  {
382
378
  type: 'posts',
383
- title: 'JR is Great',
384
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
385
- links: {
386
- author: {linkage: {type: 'people', id: '3'}}
379
+ attributes: {
380
+ title: 'JR is Great',
381
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
382
+ },
383
+ relationships: {
384
+ author: {data: {type: 'people', id: '3'}}
387
385
  }
388
386
  },
389
387
  {
390
388
  type: 'posts',
391
- title: 'Ember is Great',
392
- body: 'Ember is the greatest thing since unsliced bread.',
393
- links: {
394
- author: {linkage: {type: 'people', id: '3'}}
389
+ attributes: {
390
+ title: 'Ember is Great',
391
+ body: 'Ember is the greatest thing since unsliced bread.'
392
+ },
393
+ relationships: {
394
+ author: {data: {type: 'people', id: '3'}}
395
395
  }
396
396
  }
397
397
  ]
@@ -400,7 +400,7 @@ class PostsControllerTest < ActionController::TestCase
400
400
  assert_response :created
401
401
  assert json_response['data'].is_a?(Array)
402
402
  assert_equal json_response['data'].size, 2
403
- assert_equal json_response['data'][0]['links']['author']['linkage']['id'], '3'
403
+ assert_equal json_response['data'][0]['relationships']['author']['data']['id'], '3'
404
404
  assert_match /JR is Great/, response.body
405
405
  assert_match /Ember is Great/, response.body
406
406
  end
@@ -412,18 +412,22 @@ class PostsControllerTest < ActionController::TestCase
412
412
  data: [
413
413
  {
414
414
  type: 'posts',
415
- Title: 'JR is Great',
416
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
417
- links: {
418
- author: {linkage: {type: 'people', id: '3'}}
415
+ attributes: {
416
+ Title: 'JR is Great',
417
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
418
+ },
419
+ relationships: {
420
+ author: {data: {type: 'people', id: '3'}}
419
421
  }
420
422
  },
421
423
  {
422
424
  type: 'posts',
423
- title: 'Ember is Great',
424
- BODY: 'Ember is the greatest thing since unsliced bread.',
425
- links: {
426
- author: {linkage: {type: 'people', id: '3'}}
425
+ attributes: {
426
+ title: 'Ember is Great',
427
+ BODY: 'Ember is the greatest thing since unsliced bread.'
428
+ },
429
+ relationships: {
430
+ author: {data: {type: 'people', id: '3'}}
427
431
  }
428
432
  }
429
433
  ]
@@ -439,10 +443,12 @@ class PostsControllerTest < ActionController::TestCase
439
443
  {
440
444
  data_spelled_wrong: {
441
445
  type: 'posts',
442
- title: 'JR is Great',
443
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
444
- links: {
445
- author: {linkage: {type: 'people', id: '3'}}
446
+ attributes: {
447
+ title: 'JR is Great',
448
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
449
+ },
450
+ relationships: {
451
+ author: {data: {type: 'people', id: '3'}}
446
452
  }
447
453
  }
448
454
  }
@@ -457,10 +463,12 @@ class PostsControllerTest < ActionController::TestCase
457
463
  {
458
464
  data: {
459
465
  type: 'posts_spelled_wrong',
460
- title: 'JR is Great',
461
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
462
- links: {
463
- author: {linkage: {type: 'people', id: '3'}}
466
+ attributes: {
467
+ title: 'JR is Great',
468
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
469
+ },
470
+ relationships: {
471
+ author: {data: {type: 'people', id: '3'}}
464
472
  }
465
473
  }
466
474
  }
@@ -474,10 +482,12 @@ class PostsControllerTest < ActionController::TestCase
474
482
  post :create,
475
483
  {
476
484
  data: {
477
- title: 'JR is Great',
478
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
479
- links: {
480
- author: {linkage: {type: 'people', id: '3'}}
485
+ attributes: {
486
+ title: 'JR is Great',
487
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
488
+ },
489
+ relationships: {
490
+ author: {data: {type: 'people', id: '3'}}
481
491
  }
482
492
  }
483
493
  }
@@ -492,10 +502,12 @@ class PostsControllerTest < ActionController::TestCase
492
502
  {
493
503
  data: {
494
504
  type: 'posts',
495
- subject: 'JR is Great',
496
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
497
- links: {
498
- author: {linkage: {type: 'people', id: '3'}}
505
+ attributes: {
506
+ subject: 'JR is Great',
507
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
508
+ },
509
+ relationships: {
510
+ author: {data: {type: 'people', id: '3'}}
499
511
  }
500
512
  }
501
513
  }
@@ -510,20 +522,22 @@ class PostsControllerTest < ActionController::TestCase
510
522
  {
511
523
  data: {
512
524
  type: 'posts',
513
- title: 'JR is Great',
514
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
515
- links: {
516
- author: {linkage: {type: 'people', id: '3'}},
517
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
525
+ attributes: {
526
+ title: 'JR is Great',
527
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
528
+ },
529
+ relationships: {
530
+ author: {data: {type: 'people', id: '3'}},
531
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
518
532
  }
519
533
  }
520
534
  }
521
535
 
522
536
  assert_response :created
523
537
  assert json_response['data'].is_a?(Hash)
524
- assert_equal '3', json_response['data']['links']['author']['linkage']['id']
525
- assert_equal 'JR is Great', json_response['data']['title']
526
- assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['body']
538
+ assert_equal '3', json_response['data']['relationships']['author']['data']['id']
539
+ assert_equal 'JR is Great', json_response['data']['attributes']['title']
540
+ assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['attributes']['body']
527
541
  end
528
542
 
529
543
  def test_create_with_links_has_many_array
@@ -532,20 +546,22 @@ class PostsControllerTest < ActionController::TestCase
532
546
  {
533
547
  data: {
534
548
  type: 'posts',
535
- title: 'JR is Great',
536
- body: 'JSONAPIResources is the greatest thing since unsliced bread.',
537
- links: {
538
- author: {linkage: {type: 'people', id: '3'}},
539
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
549
+ attributes: {
550
+ title: 'JR is Great',
551
+ body: 'JSONAPIResources is the greatest thing since unsliced bread.'
552
+ },
553
+ relationships: {
554
+ author: {data: {type: 'people', id: '3'}},
555
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
540
556
  }
541
557
  }
542
558
  }
543
559
 
544
560
  assert_response :created
545
561
  assert json_response['data'].is_a?(Hash)
546
- assert_equal '3', json_response['data']['links']['author']['linkage']['id']
547
- assert_equal 'JR is Great', json_response['data']['title']
548
- assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['body']
562
+ assert_equal '3', json_response['data']['relationships']['author']['data']['id']
563
+ assert_equal 'JR is Great', json_response['data']['attributes']['title']
564
+ assert_equal 'JSONAPIResources is the greatest thing since unsliced bread.', json_response['data']['attributes']['body']
549
565
  end
550
566
 
551
567
  def test_create_with_links_include_and_fields
@@ -554,11 +570,13 @@ class PostsControllerTest < ActionController::TestCase
554
570
  {
555
571
  data: {
556
572
  type: 'posts',
557
- title: 'JR is Great!',
558
- body: 'JSONAPIResources is the greatest thing since unsliced bread!',
559
- links: {
560
- author: {linkage: {type: 'people', id: '3'}},
561
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
573
+ attributes: {
574
+ title: 'JR is Great!',
575
+ body: 'JSONAPIResources is the greatest thing since unsliced bread!'
576
+ },
577
+ relationships: {
578
+ author: {data: {type: 'people', id: '3'}},
579
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
562
580
  }
563
581
  },
564
582
  include: 'author,author.posts',
@@ -567,8 +585,8 @@ class PostsControllerTest < ActionController::TestCase
567
585
 
568
586
  assert_response :created
569
587
  assert json_response['data'].is_a?(Hash)
570
- assert_equal '3', json_response['data']['links']['author']['linkage']['id']
571
- assert_equal 'JR is Great!', json_response['data']['title']
588
+ assert_equal '3', json_response['data']['relationships']['author']['data']['id']
589
+ assert_equal 'JR is Great!', json_response['data']['attributes']['title']
572
590
  assert_not_nil json_response['included'].size
573
591
  end
574
592
 
@@ -582,10 +600,12 @@ class PostsControllerTest < ActionController::TestCase
582
600
  data: {
583
601
  id: '3',
584
602
  type: 'posts',
585
- title: 'A great new Post',
586
- links: {
587
- section: {linkage: {type: 'sections', id: "#{javascript.id}"}},
588
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
603
+ attributes: {
604
+ title: 'A great new Post'
605
+ },
606
+ relationships: {
607
+ section: {data: {type: 'sections', id: "#{javascript.id}"}},
608
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
589
609
  }
590
610
  },
591
611
  include: 'tags'
@@ -593,12 +613,12 @@ class PostsControllerTest < ActionController::TestCase
593
613
 
594
614
  assert_response :success
595
615
  assert json_response['data'].is_a?(Hash)
596
- assert_equal '3', json_response['data']['links']['author']['linkage']['id']
597
- assert_equal javascript.id.to_s, json_response['data']['links']['section']['linkage']['id']
598
- assert_equal 'A great new Post', json_response['data']['title']
599
- assert_equal 'AAAA', json_response['data']['body']
616
+ assert_equal '3', json_response['data']['relationships']['author']['data']['id']
617
+ assert_equal javascript.id.to_s, json_response['data']['relationships']['section']['data']['id']
618
+ assert_equal 'A great new Post', json_response['data']['attributes']['title']
619
+ assert_equal 'AAAA', json_response['data']['attributes']['body']
600
620
  assert matches_array?([{'type' => 'tags', 'id' => '3'}, {'type' => 'tags', 'id' => '4'}],
601
- json_response['data']['links']['tags']['linkage'])
621
+ json_response['data']['relationships']['tags']['data'])
602
622
  end
603
623
 
604
624
  def test_update_remove_links
@@ -609,10 +629,12 @@ class PostsControllerTest < ActionController::TestCase
609
629
  data: {
610
630
  id: '3',
611
631
  type: 'posts',
612
- title: 'A great new Post',
613
- links: {
614
- section: {linkage: {type: 'sections', id: 1}},
615
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
632
+ attributes: {
633
+ title: 'A great new Post'
634
+ },
635
+ relationships: {
636
+ section: {data: {type: 'sections', id: 1}},
637
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
616
638
  }
617
639
  },
618
640
  include: 'tags'
@@ -626,8 +648,10 @@ class PostsControllerTest < ActionController::TestCase
626
648
  data: {
627
649
  type: 'posts',
628
650
  id: 3,
629
- title: 'A great new Post',
630
- links: {
651
+ attributes: {
652
+ title: 'A great new Post'
653
+ },
654
+ relationships: {
631
655
  section: nil,
632
656
  tags: []
633
657
  }
@@ -637,12 +661,12 @@ class PostsControllerTest < ActionController::TestCase
637
661
 
638
662
  assert_response :success
639
663
  assert json_response['data'].is_a?(Hash)
640
- assert_equal '3', json_response['data']['links']['author']['linkage']['id']
641
- assert_equal nil, json_response['data']['links']['section']['linkage']
642
- assert_equal 'A great new Post', json_response['data']['title']
643
- assert_equal 'AAAA', json_response['data']['body']
664
+ assert_equal '3', json_response['data']['relationships']['author']['data']['id']
665
+ assert_equal nil, json_response['data']['relationships']['section']['data']
666
+ assert_equal 'A great new Post', json_response['data']['attributes']['title']
667
+ assert_equal 'AAAA', json_response['data']['attributes']['body']
644
668
  assert matches_array?([],
645
- json_response['data']['links']['tags']['linkage'])
669
+ json_response['data']['relationships']['tags']['data'])
646
670
  end
647
671
 
648
672
  def test_update_relationship_has_one
@@ -698,7 +722,7 @@ class PostsControllerTest < ActionController::TestCase
698
722
  data: {
699
723
  type: 'posts',
700
724
  id: 3,
701
- links: {
725
+ relationships: {
702
726
  tags: nil
703
727
  }
704
728
  }
@@ -716,8 +740,8 @@ class PostsControllerTest < ActionController::TestCase
716
740
  data: {
717
741
  type: 'posts',
718
742
  id: 3,
719
- links: {
720
- tags: {linkage: {typ: 'bad link', idd: 'as'}}
743
+ relationships: {
744
+ tags: {data: {typ: 'bad link', idd: 'as'}}
721
745
  }
722
746
  }
723
747
  }
@@ -734,7 +758,7 @@ class PostsControllerTest < ActionController::TestCase
734
758
  data: {
735
759
  type: 'posts',
736
760
  id: 3,
737
- links: {
761
+ relationships: {
738
762
  tags: 'bad link'
739
763
  }
740
764
  }
@@ -744,7 +768,7 @@ class PostsControllerTest < ActionController::TestCase
744
768
  assert_match /Invalid Links Object/, response.body
745
769
  end
746
770
 
747
- def test_update_other_has_many_links_linkage_nil
771
+ def test_update_other_has_many_links_data_nil
748
772
  set_content_type_header!
749
773
  put :update,
750
774
  {
@@ -752,8 +776,8 @@ class PostsControllerTest < ActionController::TestCase
752
776
  data: {
753
777
  type: 'posts',
754
778
  id: 3,
755
- links: {
756
- tags: {linkage: nil}
779
+ relationships: {
780
+ tags: {data: nil}
757
781
  }
758
782
  }
759
783
  }
@@ -979,8 +1003,10 @@ class PostsControllerTest < ActionController::TestCase
979
1003
  data: {
980
1004
  type: 'posts',
981
1005
  id: 2,
982
- title: 'A great new Post',
983
- links: {
1006
+ attributes: {
1007
+ title: 'A great new Post'
1008
+ },
1009
+ relationships: {
984
1010
  section: {type: 'sections', id: "#{javascript.id}"},
985
1011
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
986
1012
  }
@@ -1001,9 +1027,11 @@ class PostsControllerTest < ActionController::TestCase
1001
1027
  data: {
1002
1028
  type: 'posts',
1003
1029
  id: '3',
1004
- asdfg: 'aaaa',
1005
- title: 'A great new Post',
1006
- links: {
1030
+ attributes: {
1031
+ asdfg: 'aaaa',
1032
+ title: 'A great new Post'
1033
+ },
1034
+ relationships: {
1007
1035
  section: {type: 'sections', id: "#{javascript.id}"},
1008
1036
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1009
1037
  }
@@ -1024,8 +1052,10 @@ class PostsControllerTest < ActionController::TestCase
1024
1052
  data: {
1025
1053
  type: 'posts',
1026
1054
  id: '3',
1027
- title: 'A great new Post',
1028
- links: {
1055
+ attributes: {
1056
+ title: 'A great new Post'
1057
+ },
1058
+ relationships: {
1029
1059
  asdfg: 'aaaa',
1030
1060
  section: {type: 'sections', id: "#{javascript.id}"},
1031
1061
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
@@ -1046,8 +1076,10 @@ class PostsControllerTest < ActionController::TestCase
1046
1076
  id: 3,
1047
1077
  data_spelled_wrong: {
1048
1078
  type: 'posts',
1049
- title: 'A great new Post',
1050
- links: {
1079
+ attributes: {
1080
+ title: 'A great new Post'
1081
+ },
1082
+ relationships: {
1051
1083
  section: {type: 'sections', id: "#{javascript.id}"},
1052
1084
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1053
1085
  }
@@ -1066,7 +1098,9 @@ class PostsControllerTest < ActionController::TestCase
1066
1098
  id: 3,
1067
1099
  data: {
1068
1100
  type: 'posts',
1069
- title: 'A great new Post'
1101
+ attributes: {
1102
+ title: 'A great new Post'
1103
+ }
1070
1104
  }
1071
1105
  }
1072
1106
 
@@ -1084,8 +1118,10 @@ class PostsControllerTest < ActionController::TestCase
1084
1118
  data: {
1085
1119
  id: '3',
1086
1120
  type_spelled_wrong: 'posts',
1087
- title: 'A great new Post',
1088
- links: {
1121
+ attributes: {
1122
+ title: 'A great new Post'
1123
+ },
1124
+ relationships: {
1089
1125
  section: {type: 'sections', id: "#{javascript.id}"},
1090
1126
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1091
1127
  }
@@ -1096,6 +1132,31 @@ class PostsControllerTest < ActionController::TestCase
1096
1132
  assert_match /The required parameter, type, is missing./, response.body
1097
1133
  end
1098
1134
 
1135
+ def test_update_unknown_key
1136
+ set_content_type_header!
1137
+ javascript = Section.find_by(name: 'javascript')
1138
+
1139
+ put :update,
1140
+ {
1141
+ id: 3,
1142
+ data: {
1143
+ id: '3',
1144
+ type: 'posts',
1145
+ body: 'asdfg',
1146
+ attributes: {
1147
+ title: 'A great new Post'
1148
+ },
1149
+ relationships: {
1150
+ section: {type: 'sections', id: "#{javascript.id}"},
1151
+ tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ assert_response :bad_request
1157
+ assert_match /body is not allowed/, response.body
1158
+ end
1159
+
1099
1160
  def test_update_multiple
1100
1161
  set_content_type_header!
1101
1162
  javascript = Section.find_by(name: 'javascript')
@@ -1107,19 +1168,23 @@ class PostsControllerTest < ActionController::TestCase
1107
1168
  {
1108
1169
  type: 'posts',
1109
1170
  id: 3,
1110
- title: 'A great new Post QWERTY',
1111
- links: {
1112
- section: {linkage: {type: 'sections', id: "#{javascript.id}"}},
1113
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1171
+ attributes: {
1172
+ title: 'A great new Post QWERTY'
1173
+ },
1174
+ relationships: {
1175
+ section: {data: {type: 'sections', id: "#{javascript.id}"}},
1176
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1114
1177
  }
1115
1178
  },
1116
1179
  {
1117
1180
  type: 'posts',
1118
1181
  id: 16,
1119
- title: 'A great new Post ASDFG',
1120
- links: {
1121
- section: {linkage: {type: 'sections', id: "#{javascript.id}"}},
1122
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1182
+ attributes: {
1183
+ title: 'A great new Post ASDFG'
1184
+ },
1185
+ relationships: {
1186
+ section: {data: {type: 'sections', id: "#{javascript.id}"}},
1187
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1123
1188
  }
1124
1189
  }
1125
1190
  ],
@@ -1128,19 +1193,19 @@ class PostsControllerTest < ActionController::TestCase
1128
1193
 
1129
1194
  assert_response :success
1130
1195
  assert_equal json_response['data'].size, 2
1131
- assert_equal json_response['data'][0]['links']['author']['linkage']['id'], '3'
1132
- assert_equal json_response['data'][0]['links']['section']['linkage']['id'], javascript.id.to_s
1133
- assert_equal json_response['data'][0]['title'], 'A great new Post QWERTY'
1134
- assert_equal json_response['data'][0]['body'], 'AAAA'
1196
+ assert_equal json_response['data'][0]['relationships']['author']['data']['id'], '3'
1197
+ assert_equal json_response['data'][0]['relationships']['section']['data']['id'], javascript.id.to_s
1198
+ assert_equal json_response['data'][0]['attributes']['title'], 'A great new Post QWERTY'
1199
+ assert_equal json_response['data'][0]['attributes']['body'], 'AAAA'
1135
1200
  assert matches_array?([{'type' => 'tags', 'id' => '3'}, {'type' => 'tags', 'id' => '4'}],
1136
- json_response['data'][0]['links']['tags']['linkage'])
1201
+ json_response['data'][0]['relationships']['tags']['data'])
1137
1202
 
1138
- assert_equal json_response['data'][1]['links']['author']['linkage']['id'], '3'
1139
- assert_equal json_response['data'][1]['links']['section']['linkage']['id'], javascript.id.to_s
1140
- assert_equal json_response['data'][1]['title'], 'A great new Post ASDFG'
1141
- assert_equal json_response['data'][1]['body'], 'Not First!!!!'
1203
+ assert_equal json_response['data'][1]['relationships']['author']['data']['id'], '3'
1204
+ assert_equal json_response['data'][1]['relationships']['section']['data']['id'], javascript.id.to_s
1205
+ assert_equal json_response['data'][1]['attributes']['title'], 'A great new Post ASDFG'
1206
+ assert_equal json_response['data'][1]['attributes']['body'], 'Not First!!!!'
1142
1207
  assert matches_array?([{'type' => 'tags', 'id' => '3'}, {'type' => 'tags', 'id' => '4'}],
1143
- json_response['data'][1]['links']['tags']['linkage'])
1208
+ json_response['data'][1]['relationships']['tags']['data'])
1144
1209
  end
1145
1210
 
1146
1211
  def test_update_multiple_missing_keys
@@ -1153,16 +1218,20 @@ class PostsControllerTest < ActionController::TestCase
1153
1218
  data: [
1154
1219
  {
1155
1220
  type: 'posts',
1156
- title: 'A great new Post ASDFG',
1157
- links: {
1221
+ attributes: {
1222
+ title: 'A great new Post ASDFG'
1223
+ },
1224
+ relationships: {
1158
1225
  section: {type: 'sections', id: "#{javascript.id}"},
1159
1226
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1160
1227
  }
1161
1228
  },
1162
1229
  {
1163
1230
  type: 'posts',
1164
- title: 'A great new Post QWERTY',
1165
- links: {
1231
+ attributes: {
1232
+ title: 'A great new Post QWERTY'
1233
+ },
1234
+ relationships: {
1166
1235
  section: {type: 'sections', id: "#{javascript.id}"},
1167
1236
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1168
1237
  }
@@ -1184,19 +1253,23 @@ class PostsControllerTest < ActionController::TestCase
1184
1253
  {
1185
1254
  type: 'posts',
1186
1255
  id: 3,
1187
- title: 'A great new Post ASDFG',
1188
- links: {
1189
- section: {linkage: {type: 'sections', id: "#{javascript.id}"}},
1190
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1256
+ attributes: {
1257
+ title: 'A great new Post ASDFG'
1258
+ },
1259
+ relationships: {
1260
+ section: {data: {type: 'sections', id: "#{javascript.id}"}},
1261
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1191
1262
  }
1192
1263
  },
1193
1264
  {
1194
1265
  type: 'posts',
1195
1266
  id: 8,
1196
- title: 'A great new Post QWERTY',
1197
- links: {
1198
- section: {linkage: {type: 'sections', id: "#{javascript.id}"}},
1199
- tags: {linkage: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1267
+ attributes: {
1268
+ title: 'A great new Post QWERTY'
1269
+ },
1270
+ relationships: {
1271
+ section: {data: {type: 'sections', id: "#{javascript.id}"}},
1272
+ tags: {data: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]}
1200
1273
  }
1201
1274
  }
1202
1275
  ]}
@@ -1216,8 +1289,10 @@ class PostsControllerTest < ActionController::TestCase
1216
1289
  {
1217
1290
  type: 'posts',
1218
1291
  id: 3,
1219
- title: 'A great new Post QWERTY',
1220
- links: {
1292
+ attributes: {
1293
+ title: 'A great new Post QWERTY'
1294
+ },
1295
+ relationships: {
1221
1296
  section: {type: 'sections', id: "#{javascript.id}"},
1222
1297
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1223
1298
  }
@@ -1225,8 +1300,10 @@ class PostsControllerTest < ActionController::TestCase
1225
1300
  {
1226
1301
  type: 'posts',
1227
1302
  id: 9,
1228
- title: 'A great new Post ASDFG',
1229
- links: {
1303
+ attributes: {
1304
+ title: 'A great new Post ASDFG'
1305
+ },
1306
+ relationships: {
1230
1307
  section: {type: 'sections', id: "#{javascript.id}"},
1231
1308
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1232
1309
  }
@@ -1245,8 +1322,10 @@ class PostsControllerTest < ActionController::TestCase
1245
1322
  data: {
1246
1323
  type: 'posts',
1247
1324
  id: '3',
1248
- subject: 'A great new Post',
1249
- links: {
1325
+ attributes: {
1326
+ subject: 'A great new Post'
1327
+ },
1328
+ relationships: {
1250
1329
  author: {type: 'people', id: '1'},
1251
1330
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
1252
1331
  }
@@ -1265,7 +1344,9 @@ class PostsControllerTest < ActionController::TestCase
1265
1344
  id: 3,
1266
1345
  data: {
1267
1346
  type: 'posts',
1268
- subject: 'A great new Post',
1347
+ attributes: {
1348
+ subject: 'A great new Post'
1349
+ },
1269
1350
  linked_objects: {
1270
1351
  author: {type: 'people', id: '1'},
1271
1352
  tags: [{type: 'tags', id: 3}, {type: 'tags', id: 4}]
@@ -1313,7 +1394,7 @@ class PostsControllerTest < ActionController::TestCase
1313
1394
  id: '1'
1314
1395
  },
1315
1396
  links: {
1316
- self: 'http://test.host/posts/1/links/author',
1397
+ self: 'http://test.host/posts/1/relationships/author',
1317
1398
  related: 'http://test.host/posts/1/author'
1318
1399
  }
1319
1400
  }
@@ -1328,7 +1409,7 @@ class PostsControllerTest < ActionController::TestCase
1328
1409
  {type: 'tags', id: '5'}
1329
1410
  ],
1330
1411
  links: {
1331
- self: 'http://test.host/posts/2/links/tags',
1412
+ self: 'http://test.host/posts/2/relationships/tags',
1332
1413
  related: 'http://test.host/posts/2/tags'
1333
1414
  }
1334
1415
  }
@@ -1347,7 +1428,7 @@ class PostsControllerTest < ActionController::TestCase
1347
1428
  {
1348
1429
  data: nil,
1349
1430
  links: {
1350
- self: 'http://test.host/posts/17/links/author',
1431
+ self: 'http://test.host/posts/17/relationships/author',
1351
1432
  related: 'http://test.host/posts/17/author'
1352
1433
  }
1353
1434
  }
@@ -1356,10 +1437,10 @@ end
1356
1437
 
1357
1438
  class TagsControllerTest < ActionController::TestCase
1358
1439
  def test_tags_index
1359
- get :index, {filter: {id: '6,7,8,9'}, include: 'posts,posts.tags,posts.author.posts'}
1440
+ get :index, {filter: {id: '6,7,8,9'}, include: 'posts.tags,posts.author.posts'}
1360
1441
  assert_response :success
1361
1442
  assert_equal 4, json_response['data'].size
1362
- assert_equal 2, json_response['included'].size
1443
+ assert_equal 3, json_response['included'].size
1363
1444
  end
1364
1445
 
1365
1446
  def test_tags_show_multiple
@@ -1369,7 +1450,7 @@ class TagsControllerTest < ActionController::TestCase
1369
1450
  end
1370
1451
 
1371
1452
  def test_tags_show_multiple_with_include
1372
- get :show, {id: '6,7,8,9', include: 'posts,posts.tags,posts.author.posts'}
1453
+ get :show, {id: '6,7,8,9', include: 'posts.tags,posts.author.posts'}
1373
1454
  assert_response :bad_request
1374
1455
  assert_match /6,7,8,9 is not a valid value for id/, response.body
1375
1456
  end
@@ -1396,7 +1477,7 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1396
1477
  JSONAPI.configuration.use_text_errors = true
1397
1478
  get :index, {sort: 'not_in_record'}
1398
1479
  assert_response 400
1399
- assert_equal 'INVALID_SORT_FORMAT', json_response['errors'][0]['code']
1480
+ assert_equal 'INVALID_SORT_CRITERIA', json_response['errors'][0]['code']
1400
1481
  JSONAPI.configuration.use_text_errors = false
1401
1482
  end
1402
1483
 
@@ -1437,7 +1518,7 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1437
1518
  get :show, {id: 1, include: 'isoCurrency,employee', 'fields' => {'expenseEntries' => 'transactionDate'}}
1438
1519
  assert_response :success
1439
1520
  assert json_response['data'].is_a?(Hash)
1440
- assert json_response['data'].has_key?('transactionDate')
1521
+ assert json_response['data']['attributes'].has_key?('transactionDate')
1441
1522
  assert_equal 2, json_response['included'].size
1442
1523
  end
1443
1524
 
@@ -1446,7 +1527,7 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1446
1527
  'isoCurrencies' => 'id,name'}}
1447
1528
  assert_response :success
1448
1529
  assert json_response['data'].is_a?(Hash)
1449
- assert json_response['data'].has_key?('transactionDate')
1530
+ assert json_response['data']['attributes'].has_key?('transactionDate')
1450
1531
  assert_equal 2, json_response['included'].size
1451
1532
  end
1452
1533
 
@@ -1458,11 +1539,13 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1458
1539
  {
1459
1540
  data: {
1460
1541
  type: 'expense_entries',
1461
- transaction_date: '2014/04/15',
1462
- cost: 50.58,
1463
- links: {
1464
- employee: {linkage: {type: 'people', id: '3'}},
1465
- iso_currency: {linkage: {type: 'iso_currencies', id: 'USD'}}
1542
+ attributes: {
1543
+ transaction_date: '2014/04/15',
1544
+ cost: 50.58
1545
+ },
1546
+ relationships: {
1547
+ employee: {data: {type: 'people', id: '3'}},
1548
+ iso_currency: {data: {type: 'iso_currencies', id: 'USD'}}
1466
1549
  }
1467
1550
  },
1468
1551
  include: 'iso_currency',
@@ -1471,9 +1554,9 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1471
1554
 
1472
1555
  assert_response :created
1473
1556
  assert json_response['data'].is_a?(Hash)
1474
- assert_equal '3', json_response['data']['links']['employee']['linkage']['id']
1475
- assert_equal 'USD', json_response['data']['links']['iso_currency']['linkage']['id']
1476
- assert_equal '50.58', json_response['data']['cost']
1557
+ assert_equal '3', json_response['data']['relationships']['employee']['data']['id']
1558
+ assert_equal 'USD', json_response['data']['relationships']['iso_currency']['data']['id']
1559
+ assert_equal '50.58', json_response['data']['attributes']['cost']
1477
1560
 
1478
1561
  delete :destroy, {id: json_response['data']['id']}
1479
1562
  assert_response :no_content
@@ -1487,11 +1570,13 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1487
1570
  {
1488
1571
  data: {
1489
1572
  type: 'expense_entries',
1490
- transactionDate: '2014/04/15',
1491
- cost: 50.58,
1492
- links: {
1493
- employee: {linkage: {type: 'people', id: '3'}},
1494
- isoCurrency: {linkage: {type: 'iso_currencies', id: 'USD'}}
1573
+ attributes: {
1574
+ transactionDate: '2014/04/15',
1575
+ cost: 50.58
1576
+ },
1577
+ relationships: {
1578
+ employee: {data: {type: 'people', id: '3'}},
1579
+ isoCurrency: {data: {type: 'iso_currencies', id: 'USD'}}
1495
1580
  }
1496
1581
  },
1497
1582
  include: 'isoCurrency',
@@ -1500,9 +1585,9 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1500
1585
 
1501
1586
  assert_response :created
1502
1587
  assert json_response['data'].is_a?(Hash)
1503
- assert_equal '3', json_response['data']['links']['employee']['linkage']['id']
1504
- assert_equal 'USD', json_response['data']['links']['isoCurrency']['linkage']['id']
1505
- assert_equal '50.58', json_response['data']['cost']
1588
+ assert_equal '3', json_response['data']['relationships']['employee']['data']['id']
1589
+ assert_equal 'USD', json_response['data']['relationships']['isoCurrency']['data']['id']
1590
+ assert_equal '50.58', json_response['data']['attributes']['cost']
1506
1591
 
1507
1592
  delete :destroy, {id: json_response['data']['id']}
1508
1593
  assert_response :no_content
@@ -1516,11 +1601,13 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1516
1601
  {
1517
1602
  data: {
1518
1603
  type: 'expense_entries',
1519
- 'transaction-date' => '2014/04/15',
1520
- cost: 50.58,
1521
- links: {
1522
- employee: {linkage: {type: 'people', id: '3'}},
1523
- 'iso-currency' => {linkage: {type: 'iso_currencies', id: 'USD'}}
1604
+ attributes: {
1605
+ 'transaction-date' => '2014/04/15',
1606
+ cost: 50.58
1607
+ },
1608
+ relationships: {
1609
+ employee: {data: {type: 'people', id: '3'}},
1610
+ 'iso-currency' => {data: {type: 'iso_currencies', id: 'USD'}}
1524
1611
  }
1525
1612
  },
1526
1613
  include: 'iso-currency',
@@ -1529,9 +1616,9 @@ class ExpenseEntriesControllerTest < ActionController::TestCase
1529
1616
 
1530
1617
  assert_response :created
1531
1618
  assert json_response['data'].is_a?(Hash)
1532
- assert_equal '3', json_response['data']['links']['employee']['linkage']['id']
1533
- assert_equal 'USD', json_response['data']['links']['iso-currency']['linkage']['id']
1534
- assert_equal '50.58', json_response['data']['cost']
1619
+ assert_equal '3', json_response['data']['relationships']['employee']['data']['id']
1620
+ assert_equal 'USD', json_response['data']['relationships']['iso-currency']['data']['id']
1621
+ assert_equal '50.58', json_response['data']['attributes']['cost']
1535
1622
 
1536
1623
  delete :destroy, {id: json_response['data']['id']}
1537
1624
  assert_response :no_content
@@ -1544,63 +1631,104 @@ class IsoCurrenciesControllerTest < ActionController::TestCase
1544
1631
  end
1545
1632
 
1546
1633
  def test_currencies_show
1547
- get :show, {code: 'USD'}
1634
+ get :show, {id: 'USD'}
1548
1635
  assert_response :success
1549
1636
  assert json_response['data'].is_a?(Hash)
1550
1637
  end
1551
1638
 
1639
+ def test_create_currencies_client_generated_id
1640
+ set_content_type_header!
1641
+ JSONAPI.configuration.json_key_format = :underscored_route
1642
+
1643
+ post :create,
1644
+ {
1645
+ data: {
1646
+ type: 'iso_currencies',
1647
+ id: 'BTC',
1648
+ attributes: {
1649
+ name: 'Bit Coin',
1650
+ 'country_name' => 'global',
1651
+ 'minor_unit' => 'satoshi'
1652
+ }
1653
+ }
1654
+ }
1655
+
1656
+ assert_response :created
1657
+ assert_equal 'BTC', json_response['data']['id']
1658
+ assert_equal 'Bit Coin', json_response['data']['attributes']['name']
1659
+ assert_equal 'global', json_response['data']['attributes']['country_name']
1660
+ assert_equal 'satoshi', json_response['data']['attributes']['minor_unit']
1661
+
1662
+ delete :destroy, {id: json_response['data']['id']}
1663
+ assert_response :no_content
1664
+ end
1665
+
1666
+ def test_currencies_primary_key_sort
1667
+ get :index, {sort: 'id'}
1668
+ assert_response :success
1669
+ assert_equal 3, json_response['data'].size
1670
+ assert_equal 'CAD', json_response['data'][0]['id']
1671
+ assert_equal 'EUR', json_response['data'][1]['id']
1672
+ assert_equal 'USD', json_response['data'][2]['id']
1673
+ end
1674
+
1675
+ def test_currencies_code_sort
1676
+ get :index, {sort: 'code'}
1677
+ assert_response :bad_request
1678
+ end
1679
+
1552
1680
  def test_currencies_json_key_underscored_sort
1553
1681
  JSONAPI.configuration.json_key_format = :underscored_key
1554
- get :index, {sort: '+country_name'}
1682
+ get :index, {sort: 'country_name'}
1555
1683
  assert_response :success
1556
1684
  assert_equal 3, json_response['data'].size
1557
- assert_equal 'Canada', json_response['data'][0]['country_name']
1558
- assert_equal 'Euro Member Countries', json_response['data'][1]['country_name']
1559
- assert_equal 'United States', json_response['data'][2]['country_name']
1685
+ assert_equal 'Canada', json_response['data'][0]['attributes']['country_name']
1686
+ assert_equal 'Euro Member Countries', json_response['data'][1]['attributes']['country_name']
1687
+ assert_equal 'United States', json_response['data'][2]['attributes']['country_name']
1560
1688
 
1561
1689
  # reverse sort
1562
1690
  get :index, {sort: '-country_name'}
1563
1691
  assert_response :success
1564
1692
  assert_equal 3, json_response['data'].size
1565
- assert_equal 'United States', json_response['data'][0]['country_name']
1566
- assert_equal 'Euro Member Countries', json_response['data'][1]['country_name']
1567
- assert_equal 'Canada', json_response['data'][2]['country_name']
1693
+ assert_equal 'United States', json_response['data'][0]['attributes']['country_name']
1694
+ assert_equal 'Euro Member Countries', json_response['data'][1]['attributes']['country_name']
1695
+ assert_equal 'Canada', json_response['data'][2]['attributes']['country_name']
1568
1696
  end
1569
1697
 
1570
1698
  def test_currencies_json_key_dasherized_sort
1571
1699
  JSONAPI.configuration.json_key_format = :dasherized_key
1572
- get :index, {sort: '+country-name'}
1700
+ get :index, {sort: 'country-name'}
1573
1701
  assert_response :success
1574
1702
  assert_equal 3, json_response['data'].size
1575
- assert_equal 'Canada', json_response['data'][0]['country-name']
1576
- assert_equal 'Euro Member Countries', json_response['data'][1]['country-name']
1577
- assert_equal 'United States', json_response['data'][2]['country-name']
1703
+ assert_equal 'Canada', json_response['data'][0]['attributes']['country-name']
1704
+ assert_equal 'Euro Member Countries', json_response['data'][1]['attributes']['country-name']
1705
+ assert_equal 'United States', json_response['data'][2]['attributes']['country-name']
1578
1706
 
1579
1707
  # reverse sort
1580
1708
  get :index, {sort: '-country-name'}
1581
1709
  assert_response :success
1582
1710
  assert_equal 3, json_response['data'].size
1583
- assert_equal 'United States', json_response['data'][0]['country-name']
1584
- assert_equal 'Euro Member Countries', json_response['data'][1]['country-name']
1585
- assert_equal 'Canada', json_response['data'][2]['country-name']
1711
+ assert_equal 'United States', json_response['data'][0]['attributes']['country-name']
1712
+ assert_equal 'Euro Member Countries', json_response['data'][1]['attributes']['country-name']
1713
+ assert_equal 'Canada', json_response['data'][2]['attributes']['country-name']
1586
1714
  end
1587
1715
 
1588
1716
  def test_currencies_json_key_custom_json_key_sort
1589
1717
  JSONAPI.configuration.json_key_format = :upper_camelized_key
1590
- get :index, {sort: '+CountryName'}
1718
+ get :index, {sort: 'CountryName'}
1591
1719
  assert_response :success
1592
1720
  assert_equal 3, json_response['data'].size
1593
- assert_equal 'Canada', json_response['data'][0]['CountryName']
1594
- assert_equal 'Euro Member Countries', json_response['data'][1]['CountryName']
1595
- assert_equal 'United States', json_response['data'][2]['CountryName']
1721
+ assert_equal 'Canada', json_response['data'][0]['attributes']['CountryName']
1722
+ assert_equal 'Euro Member Countries', json_response['data'][1]['attributes']['CountryName']
1723
+ assert_equal 'United States', json_response['data'][2]['attributes']['CountryName']
1596
1724
 
1597
1725
  # reverse sort
1598
1726
  get :index, {sort: '-CountryName'}
1599
1727
  assert_response :success
1600
1728
  assert_equal 3, json_response['data'].size
1601
- assert_equal 'United States', json_response['data'][0]['CountryName']
1602
- assert_equal 'Euro Member Countries', json_response['data'][1]['CountryName']
1603
- assert_equal 'Canada', json_response['data'][2]['CountryName']
1729
+ assert_equal 'United States', json_response['data'][0]['attributes']['CountryName']
1730
+ assert_equal 'Euro Member Countries', json_response['data'][1]['attributes']['CountryName']
1731
+ assert_equal 'Canada', json_response['data'][2]['attributes']['CountryName']
1604
1732
  end
1605
1733
 
1606
1734
  def test_currencies_json_key_underscored_filter
@@ -1608,7 +1736,7 @@ class IsoCurrenciesControllerTest < ActionController::TestCase
1608
1736
  get :index, {filter: {country_name: 'Canada'}}
1609
1737
  assert_response :success
1610
1738
  assert_equal 1, json_response['data'].size
1611
- assert_equal 'Canada', json_response['data'][0]['country_name']
1739
+ assert_equal 'Canada', json_response['data'][0]['attributes']['country_name']
1612
1740
  end
1613
1741
 
1614
1742
  def test_currencies_json_key_camelized_key_filter
@@ -1616,7 +1744,7 @@ class IsoCurrenciesControllerTest < ActionController::TestCase
1616
1744
  get :index, {filter: {'countryName' => 'Canada'}}
1617
1745
  assert_response :success
1618
1746
  assert_equal 1, json_response['data'].size
1619
- assert_equal 'Canada', json_response['data'][0]['countryName']
1747
+ assert_equal 'Canada', json_response['data'][0]['attributes']['countryName']
1620
1748
  end
1621
1749
 
1622
1750
  def test_currencies_json_key_custom_json_key_filter
@@ -1624,7 +1752,7 @@ class IsoCurrenciesControllerTest < ActionController::TestCase
1624
1752
  get :index, {filter: {'CountryName' => 'Canada'}}
1625
1753
  assert_response :success
1626
1754
  assert_equal 1, json_response['data'].size
1627
- assert_equal 'Canada', json_response['data'][0]['CountryName']
1755
+ assert_equal 'Canada', json_response['data'][0]['attributes']['CountryName']
1628
1756
  end
1629
1757
  end
1630
1758
 
@@ -1639,9 +1767,11 @@ class PeopleControllerTest < ActionController::TestCase
1639
1767
  {
1640
1768
  data: {
1641
1769
  type: 'people',
1642
- name: 'Steve Jobs',
1643
- email: 'sj@email.zzz',
1644
- dateJoined: DateTime.parse('2014-1-30 4:20:00 UTC +00:00')
1770
+ attributes: {
1771
+ name: 'Steve Jobs',
1772
+ email: 'sj@email.zzz',
1773
+ dateJoined: DateTime.parse('2014-1-30 4:20:00 UTC +00:00')
1774
+ }
1645
1775
  }
1646
1776
  }
1647
1777
 
@@ -1657,9 +1787,9 @@ class PeopleControllerTest < ActionController::TestCase
1657
1787
  data: {
1658
1788
  id: '3',
1659
1789
  type: 'people',
1660
- links: {
1790
+ relationships: {
1661
1791
  'hair-cut' => {
1662
- linkage: {
1792
+ data: {
1663
1793
  type: 'hair-cuts',
1664
1794
  id: '1'
1665
1795
  }
@@ -1676,7 +1806,9 @@ class PeopleControllerTest < ActionController::TestCase
1676
1806
  {
1677
1807
  data: {
1678
1808
  type: 'people',
1679
- email: 'sj@email.zzz'
1809
+ attributes: {
1810
+ email: 'sj@email.zzz'
1811
+ }
1680
1812
  }
1681
1813
  }
1682
1814
 
@@ -1696,7 +1828,9 @@ class PeopleControllerTest < ActionController::TestCase
1696
1828
  data: {
1697
1829
  id: '3',
1698
1830
  type: 'people',
1699
- name: ''
1831
+ attributes: {
1832
+ name: ''
1833
+ }
1700
1834
  }
1701
1835
  }
1702
1836
 
@@ -1723,7 +1857,7 @@ class PeopleControllerTest < ActionController::TestCase
1723
1857
  assert_response :success
1724
1858
  assert_equal json_response['data'].size, 1
1725
1859
  assert_equal json_response['data'][0]['id'], '1'
1726
- assert_equal json_response['data'][0]['name'], 'Joe Author'
1860
+ assert_equal json_response['data'][0]['attributes']['name'], 'Joe Author'
1727
1861
  end
1728
1862
 
1729
1863
  def test_get_related_resource
@@ -1736,31 +1870,43 @@ class PeopleControllerTest < ActionController::TestCase
1736
1870
  data: {
1737
1871
  id: '1',
1738
1872
  type: 'people',
1739
- name: 'Joe Author',
1740
- email: 'joe@xyz.fake',
1741
- "date-joined" => '2013-08-07 16:25:00 -0400',
1873
+ attributes: {
1874
+ name: 'Joe Author',
1875
+ email: 'joe@xyz.fake',
1876
+ "date-joined" => '2013-08-07 16:25:00 -0400'
1877
+ },
1742
1878
  links: {
1743
- self: 'http://test.host/people/1',
1879
+ self: 'http://test.host/people/1'
1880
+ },
1881
+ relationships: {
1744
1882
  comments: {
1745
- self: 'http://test.host/people/1/links/comments',
1746
- related: 'http://test.host/people/1/comments'
1883
+ links: {
1884
+ self: 'http://test.host/people/1/relationships/comments',
1885
+ related: 'http://test.host/people/1/comments'
1886
+ }
1747
1887
  },
1748
1888
  posts: {
1749
- self: 'http://test.host/people/1/links/posts',
1750
- related: 'http://test.host/people/1/posts'
1889
+ links: {
1890
+ self: 'http://test.host/people/1/relationships/posts',
1891
+ related: 'http://test.host/people/1/posts'
1892
+ }
1751
1893
  },
1752
1894
  preferences: {
1753
- self: 'http://test.host/people/1/links/preferences',
1754
- related: 'http://test.host/people/1/preferences',
1755
- linkage: {
1895
+ links: {
1896
+ self: 'http://test.host/people/1/relationships/preferences',
1897
+ related: 'http://test.host/people/1/preferences'
1898
+ },
1899
+ data: {
1756
1900
  type: 'preferences',
1757
1901
  id: '1'
1758
1902
  }
1759
1903
  },
1760
1904
  "hair-cut" => {
1761
- "self" => "http://test.host/people/1/links/hair_cut",
1762
- "related" => "http://test.host/people/1/hair_cut",
1763
- "linkage" => nil
1905
+ "links" => {
1906
+ "self" => "http://test.host/people/1/relationships/hair_cut",
1907
+ "related" => "http://test.host/people/1/hair_cut"
1908
+ },
1909
+ "data" => nil
1764
1910
  }
1765
1911
  }
1766
1912
  }
@@ -1787,8 +1933,8 @@ class Api::V5::AuthorsControllerTest < ActionController::TestCase
1787
1933
  assert_equal 1, json_response['data'].size
1788
1934
  assert_equal '1', json_response['data'][0]['id']
1789
1935
  assert_equal 'authors', json_response['data'][0]['type']
1790
- assert_equal 'Joe Author', json_response['data'][0]['name']
1791
- assert_equal nil, json_response['data'][0]['email']
1936
+ assert_equal 'Joe Author', json_response['data'][0]['attributes']['name']
1937
+ assert_equal nil, json_response['data'][0]['attributes']['email']
1792
1938
  end
1793
1939
 
1794
1940
  def test_get_person_as_author_by_name_filter
@@ -1796,7 +1942,7 @@ class Api::V5::AuthorsControllerTest < ActionController::TestCase
1796
1942
  assert_response :success
1797
1943
  assert_equal 3, json_response['data'].size
1798
1944
  assert_equal '1', json_response['data'][0]['id']
1799
- assert_equal 'Joe Author', json_response['data'][0]['name']
1945
+ assert_equal 'Joe Author', json_response['data'][0]['attributes']['name']
1800
1946
  end
1801
1947
  end
1802
1948
 
@@ -1807,7 +1953,7 @@ class BreedsControllerTest < ActionController::TestCase
1807
1953
  get :index
1808
1954
  assert_response :success
1809
1955
  assert_equal '0', json_response['data'][0]['id']
1810
- assert_equal 'Persian', json_response['data'][0]['name']
1956
+ assert_equal 'Persian', json_response['data'][0]['attributes']['name']
1811
1957
  end
1812
1958
 
1813
1959
  def test_poro_show
@@ -1815,7 +1961,7 @@ class BreedsControllerTest < ActionController::TestCase
1815
1961
  assert_response :success
1816
1962
  assert json_response['data'].is_a?(Hash)
1817
1963
  assert_equal '0', json_response['data']['id']
1818
- assert_equal 'Persian', json_response['data']['name']
1964
+ assert_equal 'Persian', json_response['data']['attributes']['name']
1819
1965
  end
1820
1966
 
1821
1967
  def test_poro_show_multiple
@@ -1831,13 +1977,32 @@ class BreedsControllerTest < ActionController::TestCase
1831
1977
  {
1832
1978
  data: {
1833
1979
  type: 'breeds',
1834
- name: 'tabby'
1980
+ attributes: {
1981
+ name: 'tabby'
1982
+ }
1835
1983
  }
1836
1984
  }
1837
1985
 
1838
- assert_response :created
1986
+ assert_response :accepted
1839
1987
  assert json_response['data'].is_a?(Hash)
1840
- assert_equal 'Tabby', json_response['data']['name']
1988
+ assert_equal 'Tabby', json_response['data']['attributes']['name']
1989
+ end
1990
+
1991
+ def test_poro_create_validation_error
1992
+ set_content_type_header!
1993
+ post :create,
1994
+ {
1995
+ data: {
1996
+ type: 'breeds',
1997
+ attributes: {
1998
+ name: ''
1999
+ }
2000
+ }
2001
+ }
2002
+
2003
+ assert_equal 1, json_response['errors'].size
2004
+ assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code']
2005
+ assert_match /name - can't be blank/, response.body
1841
2006
  end
1842
2007
 
1843
2008
  def test_poro_create_update
@@ -1846,13 +2011,15 @@ class BreedsControllerTest < ActionController::TestCase
1846
2011
  {
1847
2012
  data: {
1848
2013
  type: 'breeds',
1849
- name: 'CALIC'
2014
+ attributes: {
2015
+ name: 'CALIC'
2016
+ }
1850
2017
  }
1851
2018
  }
1852
2019
 
1853
- assert_response :created
2020
+ assert_response :accepted
1854
2021
  assert json_response['data'].is_a?(Hash)
1855
- assert_equal 'Calic', json_response['data']['name']
2022
+ assert_equal 'Calic', json_response['data']['attributes']['name']
1856
2023
 
1857
2024
  put :update,
1858
2025
  {
@@ -1860,12 +2027,14 @@ class BreedsControllerTest < ActionController::TestCase
1860
2027
  data: {
1861
2028
  id: json_response['data']['id'],
1862
2029
  type: 'breeds',
1863
- name: 'calico'
2030
+ attributes: {
2031
+ name: 'calico'
2032
+ }
1864
2033
  }
1865
2034
  }
1866
2035
  assert_response :success
1867
2036
  assert json_response['data'].is_a?(Hash)
1868
- assert_equal 'Calico', json_response['data']['name']
2037
+ assert_equal 'Calico', json_response['data']['attributes']['name']
1869
2038
  end
1870
2039
 
1871
2040
  def test_poro_delete
@@ -1888,17 +2057,17 @@ class Api::V1::PostsControllerTest < ActionController::TestCase
1888
2057
  def test_show_post_namespaced
1889
2058
  get :show, {id: '1'}
1890
2059
  assert_response :success
1891
- assert_equal 'http://test.host/api/v1/posts/1/links/writer', json_response['data']['links']['writer']['self']
2060
+ assert_equal 'http://test.host/api/v1/posts/1/relationships/writer', json_response['data']['relationships']['writer']['links']['self']
1892
2061
  end
1893
2062
 
1894
2063
  def test_show_post_namespaced_include
1895
2064
  get :show, {id: '1', include: 'writer'}
1896
2065
  assert_response :success
1897
- assert_equal '1', json_response['data']['links']['writer']['linkage']['id']
1898
- assert_nil json_response['data']['links']['tags']
2066
+ assert_equal '1', json_response['data']['relationships']['writer']['data']['id']
2067
+ assert_nil json_response['data']['relationships']['tags']
1899
2068
  assert_equal '1', json_response['included'][0]['id']
1900
2069
  assert_equal 'writers', json_response['included'][0]['type']
1901
- assert_equal 'joe@xyz.fake', json_response['included'][0]['email']
2070
+ assert_equal 'joe@xyz.fake', json_response['included'][0]['attributes']['email']
1902
2071
  end
1903
2072
 
1904
2073
  def test_index_filter_on_association_namespaced
@@ -1911,7 +2080,7 @@ class Api::V1::PostsControllerTest < ActionController::TestCase
1911
2080
  get :index, {sort: '-title'}
1912
2081
 
1913
2082
  assert_response :success
1914
- assert_equal "Update This Later - Multiple", json_response['data'][0]['title']
2083
+ assert_equal "Update This Later - Multiple", json_response['data'][0]['attributes']['title']
1915
2084
  end
1916
2085
 
1917
2086
  def test_create_simple_namespaced
@@ -1920,20 +2089,22 @@ class Api::V1::PostsControllerTest < ActionController::TestCase
1920
2089
  {
1921
2090
  data: {
1922
2091
  type: 'posts',
1923
- title: 'JR - now with Namespacing',
1924
- body: 'JSONAPIResources is the greatest thing since unsliced bread now that it has namespaced resources.',
1925
- links: {
1926
- writer: { linkage: {type: 'writers', id: '3'}}
2092
+ attributes: {
2093
+ title: 'JR - now with Namespacing',
2094
+ body: 'JSONAPIResources is the greatest thing since unsliced bread now that it has namespaced resources.'
2095
+ },
2096
+ relationships: {
2097
+ writer: { data: {type: 'writers', id: '3'}}
1927
2098
  }
1928
2099
  }
1929
2100
  }
1930
2101
 
1931
2102
  assert_response :created
1932
2103
  assert json_response['data'].is_a?(Hash)
1933
- assert_equal '3', json_response['data']['links']['writer']['linkage']['id']
1934
- assert_equal 'JR - now with Namespacing', json_response['data']['title']
2104
+ assert_equal '3', json_response['data']['relationships']['writer']['data']['id']
2105
+ assert_equal 'JR - now with Namespacing', json_response['data']['attributes']['title']
1935
2106
  assert_equal 'JSONAPIResources is the greatest thing since unsliced bread now that it has namespaced resources.',
1936
- json_response['data']['body']
2107
+ json_response['data']['attributes']['body']
1937
2108
  end
1938
2109
 
1939
2110
  end
@@ -1947,19 +2118,23 @@ class FactsControllerTest < ActionController::TestCase
1947
2118
  get :show, {id: '1'}
1948
2119
  assert_response :success
1949
2120
  assert json_response['data'].is_a?(Hash)
1950
- assert_equal 'Jane Author', json_response['data']['spouseName']
1951
- assert_equal 'First man to run across Antartica.', json_response['data']['bio']
1952
- assert_equal 23.89/45.6, json_response['data']['qualityRating']
1953
- assert_equal '47000.56', json_response['data']['salary']
1954
- assert_equal '2013-08-07T20:25:00Z', json_response['data']['dateTimeJoined']
1955
- assert_equal '1965-06-30', json_response['data']['birthday']
1956
- assert_equal '2000-01-01T20:00:00Z', json_response['data']['bedtime']
1957
- assert_equal 'abc', json_response['data']['photo']
1958
- assert_equal false, json_response['data']['cool']
2121
+ assert_equal 'Jane Author', json_response['data']['attributes']['spouseName']
2122
+ assert_equal 'First man to run across Antartica.', json_response['data']['attributes']['bio']
2123
+ assert_equal 23.89/45.6, json_response['data']['attributes']['qualityRating']
2124
+ assert_equal '47000.56', json_response['data']['attributes']['salary']
2125
+ assert_equal '2013-08-07T20:25:00Z', json_response['data']['attributes']['dateTimeJoined']
2126
+ assert_equal '1965-06-30', json_response['data']['attributes']['birthday']
2127
+ assert_equal '2000-01-01T20:00:00Z', json_response['data']['attributes']['bedtime']
2128
+ assert_equal 'abc', json_response['data']['attributes']['photo']
2129
+ assert_equal false, json_response['data']['attributes']['cool']
1959
2130
  end
1960
2131
  end
1961
2132
 
1962
2133
  class Api::V2::BooksControllerTest < ActionController::TestCase
2134
+ def setup
2135
+ JSONAPI.configuration.json_key_format = :dasherized_key
2136
+ end
2137
+
1963
2138
  def after_teardown
1964
2139
  Api::V2::BookResource.paginator :offset
1965
2140
  end
@@ -1970,7 +2145,31 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
1970
2145
  get :index
1971
2146
  assert_response :success
1972
2147
  assert_equal 10, json_response['data'].size
1973
- assert_equal 'Book 0', json_response['data'][0]['title']
2148
+ assert_equal 'Book 0', json_response['data'][0]['attributes']['title']
2149
+ end
2150
+
2151
+ def test_books_offset_pagination_no_params_includes_query_count_one_level
2152
+ Api::V2::BookResource.paginator :offset
2153
+
2154
+ query_count = count_queries do
2155
+ get :index, {include: 'book-comments'}
2156
+ end
2157
+ assert_response :success
2158
+ assert_equal 10, json_response['data'].size
2159
+ assert_equal 'Book 0', json_response['data'][0]['attributes']['title']
2160
+ assert_equal 2, query_count
2161
+ end
2162
+
2163
+ def test_books_offset_pagination_no_params_includes_query_count_two_levels
2164
+ Api::V2::BookResource.paginator :offset
2165
+
2166
+ query_count = count_queries do
2167
+ get :index, {include: 'book-comments,book-comments.author'}
2168
+ end
2169
+ assert_response :success
2170
+ assert_equal 10, json_response['data'].size
2171
+ assert_equal 'Book 0', json_response['data'][0]['attributes']['title']
2172
+ assert_equal 3, query_count
1974
2173
  end
1975
2174
 
1976
2175
  def test_books_offset_pagination
@@ -1979,7 +2178,7 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
1979
2178
  get :index, {page: {offset: 50, limit: 12}}
1980
2179
  assert_response :success
1981
2180
  assert_equal 12, json_response['data'].size
1982
- assert_equal 'Book 50', json_response['data'][0]['title']
2181
+ assert_equal 'Book 50', json_response['data'][0]['attributes']['title']
1983
2182
  end
1984
2183
 
1985
2184
  def test_books_offset_pagination_bad_page_param
@@ -2006,6 +2205,14 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
2006
2205
  assert_match /-1 is not a valid value for limit page parameter./, json_response['errors'][0]['detail']
2007
2206
  end
2008
2207
 
2208
+ def test_books_offset_pagination_bad_param_offset_less_than_zero
2209
+ Api::V2::BookResource.paginator :offset
2210
+
2211
+ get :index, {page: {offset: -1, limit: 20}}
2212
+ assert_response :bad_request
2213
+ assert_match /-1 is not a valid value for offset page parameter./, json_response['errors'][0]['detail']
2214
+ end
2215
+
2009
2216
  def test_books_offset_pagination_invalid_page_format
2010
2217
  Api::V2::BookResource.paginator :offset
2011
2218
 
@@ -2020,7 +2227,7 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
2020
2227
  get :index
2021
2228
  assert_response :success
2022
2229
  assert_equal 10, json_response['data'].size
2023
- assert_equal 'Book 0', json_response['data'][0]['title']
2230
+ assert_equal 'Book 0', json_response['data'][0]['attributes']['title']
2024
2231
  end
2025
2232
 
2026
2233
  def test_books_paged_pagination_no_page
@@ -2029,7 +2236,7 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
2029
2236
  get :index, {page: {size: 12}}
2030
2237
  assert_response :success
2031
2238
  assert_equal 12, json_response['data'].size
2032
- assert_equal 'Book 0', json_response['data'][0]['title']
2239
+ assert_equal 'Book 0', json_response['data'][0]['attributes']['title']
2033
2240
  end
2034
2241
 
2035
2242
  def test_books_paged_pagination
@@ -2038,7 +2245,7 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
2038
2245
  get :index, {page: {number: 3, size: 12}}
2039
2246
  assert_response :success
2040
2247
  assert_equal 12, json_response['data'].size
2041
- assert_equal 'Book 24', json_response['data'][0]['title']
2248
+ assert_equal 'Book 24', json_response['data'][0]['attributes']['title']
2042
2249
  end
2043
2250
 
2044
2251
  def test_books_paged_pagination_bad_page_param
@@ -2065,13 +2272,12 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
2065
2272
  assert_match /-1 is not a valid value for size page parameter./, json_response['errors'][0]['detail']
2066
2273
  end
2067
2274
 
2068
- def test_books_paged_pagination_invalid_page_format_interpret_int_text
2275
+ def test_books_paged_pagination_invalid_page_format_incorrect
2069
2276
  Api::V2::BookResource.paginator :paged
2070
2277
 
2071
2278
  get :index, {page: 'qwerty'}
2072
- assert_response :success
2073
- assert_equal 10, json_response['data'].size
2074
- assert_equal 'Book 0', json_response['data'][0]['title']
2279
+ assert_response :bad_request
2280
+ assert_match /0 is not a valid value for number page parameter./, json_response['errors'][0]['detail']
2075
2281
  end
2076
2282
 
2077
2283
  def test_books_paged_pagination_invalid_page_format_interpret_int
@@ -2080,6 +2286,76 @@ class Api::V2::BooksControllerTest < ActionController::TestCase
2080
2286
  get :index, {page: 3}
2081
2287
  assert_response :success
2082
2288
  assert_equal 10, json_response['data'].size
2083
- assert_equal 'Book 20', json_response['data'][0]['title']
2289
+ assert_equal 'Book 20', json_response['data'][0]['attributes']['title']
2290
+ end
2291
+ end
2292
+
2293
+ class Api::V4::BooksControllerTest < ActionController::TestCase
2294
+ def setup
2295
+ JSONAPI.configuration.json_key_format = :camelized_key
2296
+ end
2297
+
2298
+ def test_books_offset_pagination_meta
2299
+ JSONAPI.configuration.operations_processor = :counting_active_record
2300
+ Api::V4::BookResource.paginator :offset
2301
+ get :index, {page: {offset: 50, limit: 12}}
2302
+ assert_response :success
2303
+ assert_equal 12, json_response['data'].size
2304
+ assert_equal 'Book 50', json_response['data'][0]['attributes']['title']
2305
+ assert_equal 1000, json_response['meta']['totalRecords']
2306
+ JSONAPI.configuration.operations_processor = :active_record
2307
+ end
2308
+ end
2309
+
2310
+ class CategoriesControllerTest < ActionController::TestCase
2311
+ def test_index_default_filter
2312
+ get :index
2313
+ assert_response :success
2314
+ assert json_response['data'].is_a?(Array)
2315
+ assert_equal 3, json_response['data'].size
2316
+ end
2317
+
2318
+ def test_index_default_filter_override
2319
+ get :index, { filter: { status: 'inactive' } }
2320
+ assert_response :success
2321
+ assert json_response['data'].is_a?(Array)
2322
+ assert_equal 4, json_response['data'].size
2084
2323
  end
2085
2324
  end
2325
+
2326
+ class Api::V1::PlanetsControllerTest < ActionController::TestCase
2327
+ def test_save_model_callbacks
2328
+ set_content_type_header!
2329
+ post :create,
2330
+ {
2331
+ data: {
2332
+ type: 'planets',
2333
+ attributes: {
2334
+ name: 'Zeus',
2335
+ description: 'The largest planet in the solar system. Discovered in 2015.'
2336
+ }
2337
+ }
2338
+ }
2339
+
2340
+ assert_response :created
2341
+ assert json_response['data'].is_a?(Hash)
2342
+ assert_equal 'Zeus', json_response['data']['attributes']['name']
2343
+ end
2344
+
2345
+ def test_save_model_callbacks_fail
2346
+ set_content_type_header!
2347
+ post :create,
2348
+ {
2349
+ data: {
2350
+ type: 'planets',
2351
+ attributes: {
2352
+ name: 'Pluto',
2353
+ description: 'Yes, it is a planet.'
2354
+ }
2355
+ }
2356
+ }
2357
+
2358
+ assert_response :unprocessable_entity
2359
+ assert_match /Save failed or was cancelled/, json_response['errors'][0]['detail']
2360
+ end
2361
+ end