simple_jsonapi_rails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +132 -0
  4. data/CHANGELOG.md +2 -0
  5. data/Gemfile +5 -0
  6. data/Jenkinsfile +92 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +245 -0
  9. data/Rakefile +10 -0
  10. data/lib/simple_jsonapi/errors/active_model_error.rb +21 -0
  11. data/lib/simple_jsonapi/errors/active_model_error_serializer.rb +15 -0
  12. data/lib/simple_jsonapi/errors/active_record/record_not_found_serializer.rb +15 -0
  13. data/lib/simple_jsonapi/rails/action_controller/jsonapi_helper.rb +122 -0
  14. data/lib/simple_jsonapi/rails/action_controller/request_validator.rb +40 -0
  15. data/lib/simple_jsonapi/rails/action_controller.rb +44 -0
  16. data/lib/simple_jsonapi/rails/extensions/routing.rb +57 -0
  17. data/lib/simple_jsonapi/rails/extensions.rb +13 -0
  18. data/lib/simple_jsonapi/rails/railtie.rb +34 -0
  19. data/lib/simple_jsonapi/rails/test_helpers.rb +20 -0
  20. data/lib/simple_jsonapi/rails/version.rb +5 -0
  21. data/lib/simple_jsonapi/rails.rb +13 -0
  22. data/lib/simple_jsonapi_rails.rb +4 -0
  23. data/simple_jsonapi_rails.gemspec +33 -0
  24. data/test/action_controller_test.rb +299 -0
  25. data/test/dummy/.gitignore +23 -0
  26. data/test/dummy/Rakefile +6 -0
  27. data/test/dummy/app/controllers/api_controller.rb +3 -0
  28. data/test/dummy/app/controllers/orders/relationships/items_controller.rb +10 -0
  29. data/test/dummy/app/controllers/orders_controller.rb +38 -0
  30. data/test/dummy/app/models/order.rb +4 -0
  31. data/test/dummy/app/serializers/order_serializer.rb +6 -0
  32. data/test/dummy/config/application.rb +13 -0
  33. data/test/dummy/config/boot.rb +3 -0
  34. data/test/dummy/config/database.yml +7 -0
  35. data/test/dummy/config/environment.rb +5 -0
  36. data/test/dummy/config/environments/development.rb +44 -0
  37. data/test/dummy/config/environments/test.rb +44 -0
  38. data/test/dummy/config/initializers/.keep +0 -0
  39. data/test/dummy/config/locales/en.yml +2 -0
  40. data/test/dummy/config/puma.rb +56 -0
  41. data/test/dummy/config/routes.rb +5 -0
  42. data/test/dummy/config/secrets.yml +24 -0
  43. data/test/dummy/config/spring.rb +6 -0
  44. data/test/dummy/config.ru +5 -0
  45. data/test/dummy/db/migrate/20170719143227_create_orders.rb +10 -0
  46. data/test/dummy/db/seeds.rb +7 -0
  47. data/test/dummy/log/.keep +0 -0
  48. data/test/dummy/tmp/.keep +0 -0
  49. data/test/errors/active_model_error_serializer_test.rb +47 -0
  50. data/test/errors/active_model_error_test.rb +46 -0
  51. data/test/errors/active_record/record_not_found_serializer_test.rb +33 -0
  52. data/test/test_helper.rb +35 -0
  53. metadata +284 -0
@@ -0,0 +1,122 @@
1
+ require 'simple_jsonapi/rails/action_controller/request_validator'
2
+
3
+ module SimpleJsonapi
4
+ module Rails
5
+ module ActionController
6
+ class JsonapiHelper
7
+ attr_reader :controller, :pointers, :request_validator
8
+
9
+ delegate :params, :render, :request, :head, to: :controller, allow_nil: true
10
+
11
+ def initialize(controller)
12
+ @controller = controller
13
+ @pointers = {}
14
+ @request_validator = RequestValidator.new(request, params)
15
+ end
16
+
17
+ def include_params
18
+ params[:include].to_s.split(/,/).presence
19
+ end
20
+
21
+ def fields_params
22
+ (params[:fields] || {}).transform_values { |f| f.split(/,/) }
23
+ end
24
+
25
+ def filter_param(param_name)
26
+ (params[:filter] || {})[param_name]
27
+ end
28
+
29
+ def filter_param_list(param_name)
30
+ param_value = filter_param(param_name)
31
+ return nil unless param_value
32
+
33
+ param_value.split(/,/)
34
+ end
35
+
36
+ def sort_related_params
37
+ (params[:sort_related] || {}).transform_values { |f| f.split(/,/) }
38
+ end
39
+
40
+ # @param error [ActiveRecord::RecordNotFound]
41
+ def render_record_not_found(error)
42
+ render jsonapi_errors: error, status: :not_found
43
+ end
44
+
45
+ def render_model_errors(model)
46
+ errors = SimpleJsonapi::Errors::ActiveModelError.from_errors(model.errors, pointers)
47
+ render jsonapi_errors: errors, status: :unprocessable_entity
48
+ end
49
+
50
+ def render_bad_request(message)
51
+ error = SimpleJsonapi::Errors::BadRequest.new(detail: message)
52
+ render jsonapi_errors: [error], status: :bad_request
53
+ end
54
+
55
+ # private
56
+
57
+ # def url_helpers
58
+ # ::Rails.application.routes.url_helpers
59
+ # end
60
+
61
+ def deserialize(jsonapi_data)
62
+ jsonapi_hash = case jsonapi_data
63
+ when String then JSON.parse(jsonapi_data).deep_symbolize_keys
64
+ when Hash then jsonapi_data.deep_symbolize_keys
65
+ else jsonapi_data
66
+ end
67
+
68
+ data = jsonapi_hash[:data]
69
+ return unless data
70
+
71
+ result = {}
72
+ pointers = {}
73
+
74
+ result[:type] = data[:type]
75
+ result[:id] = data[:id]
76
+ pointers[:type] = "/data/type"
77
+ pointers[:id] = "/data/id"
78
+
79
+ if data[:attributes].present?
80
+ data[:attributes].each do |name, value|
81
+ result[name] = value
82
+ pointers[name] = "/data/attributes/#{name}"
83
+ end
84
+ end
85
+
86
+ if data[:relationships].present?
87
+ data[:relationships].each do |name, value|
88
+ related_data = value[:data]
89
+
90
+ if related_data.is_a?(Array)
91
+ singular_name = name.to_s.singularize
92
+ result[:"#{singular_name}_types"] = related_data.pluck(:type)
93
+ result[:"#{singular_name}_ids"] = related_data.pluck(:id)
94
+ pointers[:"#{singular_name}_types"] = "/data/relationships/#{name}"
95
+ pointers[:"#{name}_ids"] = "/data/relationships/#{name}"
96
+ elsif related_data.is_a?(Hash)
97
+ result[:"#{name}_type"] = related_data[:type]
98
+ result[:"#{name}_id"] = related_data[:id]
99
+ pointers[:"#{name}_type"] = "/data/relationships/#{name}"
100
+ pointers[:"#{name}_id"] = "/data/relationships/#{name}"
101
+ end
102
+ end
103
+ end
104
+
105
+ @pointers = pointers
106
+ result
107
+ end
108
+
109
+ def validate_jsonapi_request_headers
110
+ return head :unsupported_media_type unless request_validator.valid_content_type_header?
111
+ head :not_acceptable unless request_validator.valid_accept_header?
112
+ end
113
+
114
+ def validate_jsonapi_request_body
115
+ unless request_validator.valid_request_body?
116
+ raise InvalidJsonStructureError, "Not a valid jsonapi request body"
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,40 @@
1
+ module SimpleJsonapi
2
+ module Rails
3
+ module ActionController
4
+ class RequestValidator
5
+ attr_reader :request, :params
6
+
7
+ delegate :body, :content_type, :accept, :path, to: :request, prefix: true
8
+
9
+ def initialize(request, params)
10
+ @request = request
11
+ @params = params
12
+ end
13
+
14
+ def valid_content_type_header?
15
+ !request_has_body? || request_content_type == SimpleJsonapi::MIME_TYPE
16
+ end
17
+
18
+ def valid_accept_header?
19
+ request_accept.blank? || request_accept == SimpleJsonapi::MIME_TYPE
20
+ end
21
+
22
+ def valid_request_body?
23
+ return true unless request_has_body?
24
+
25
+ params["data"].present? && valid_relationship_body?
26
+ end
27
+
28
+ def valid_relationship_body?
29
+ request_path.exclude?("relationships") || params["data"]&.is_a?(Array)
30
+ end
31
+
32
+ private
33
+
34
+ def request_has_body?
35
+ request_body.size > 0
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module SimpleJsonapi
2
+ module Rails
3
+ module ActionController
4
+ extend ActiveSupport::Concern
5
+
6
+ delegate :validate_jsonapi_request_headers, :validate_jsonapi_request_body, to: :jsonapi
7
+
8
+ included do
9
+ before_action :validate_jsonapi_request_headers
10
+ before_action :validate_jsonapi_request_body
11
+
12
+ rescue_from ActiveRecord::RecordNotFound do |err|
13
+ jsonapi.render_record_not_found(err)
14
+ end
15
+
16
+ rescue_from ActiveRecord::RecordInvalid do |err|
17
+ jsonapi.render_model_errors(err.record)
18
+ end
19
+
20
+ rescue_from ActiveRecord::RecordNotSaved do |err|
21
+ jsonapi.render_model_errors(err.model)
22
+ end
23
+
24
+ rescue_from InvalidJsonStructureError do |err|
25
+ jsonapi.render_bad_request(err.message)
26
+ end
27
+ end
28
+
29
+ class_methods do
30
+ def jsonapi_deserialize(param_key, options = {})
31
+ prepend_before_action(options) do
32
+ if request.raw_post.present?
33
+ params[param_key] = jsonapi.deserialize(request.request_parameters)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def jsonapi
40
+ @jsonapi ||= SimpleJsonapi::Rails::ActionController::JsonapiHelper.new(self)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,57 @@
1
+ module SimpleJsonapi
2
+ module Extensions
3
+ module Routing
4
+ ACTION_MAP = {
5
+ add: :create,
6
+ remove: :destroy,
7
+ replace: :update,
8
+ }.freeze
9
+
10
+ SUPPORTED_TO_MANY_ACTIONS = ACTION_MAP.keys.freeze
11
+
12
+ def jsonapi_to_one_relationship(member_name, association)
13
+ jsonapi_relationship([:replace], member_name, association)
14
+ end
15
+
16
+ def jsonapi_to_many_relationship(member_name, association, only: nil, except: nil)
17
+ jsonapi_relationship(to_many_actions_to_define(only, except), member_name, association)
18
+ end
19
+
20
+ private
21
+
22
+ def jsonapi_relationship(actions, member_name, association)
23
+ member do
24
+ scope as: member_name, module: member_name.to_s.pluralize do
25
+ namespace "relationships" do
26
+ actions.each do |action|
27
+ resource association, only: [ACTION_MAP[action]], action: action
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def to_many_actions_to_define(only, except)
35
+ actions = if only
36
+ Array(only)
37
+ elsif except
38
+ SUPPORTED_TO_MANY_ACTIONS - Array(except)
39
+ else
40
+ SUPPORTED_TO_MANY_ACTIONS
41
+ end
42
+
43
+ ensure_actions_supported(actions)
44
+
45
+ actions
46
+ end
47
+
48
+ def ensure_actions_supported(actions)
49
+ if actions.any? { |action| SUPPORTED_TO_MANY_ACTIONS.exclude?(action) }
50
+ raise ArgumentError, "#jsonapi_to_many_relationship supports :add, :remove, and :replace actions"
51
+ end
52
+ end
53
+ end
54
+
55
+ ActionDispatch::Routing::Mapper.include(SimpleJsonapi::Extensions::Routing)
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ module SimpleJsonapi
2
+ module Rails
3
+ module RouteHelpers
4
+ private
5
+
6
+ def routes
7
+ ::Rails.application.routes.url_helpers
8
+ end
9
+ end
10
+
11
+ SimpleJsonapi::Serializer.include(RouteHelpers)
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ require 'rails/railtie'
2
+
3
+ module SimpleJsonapi
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ initializer 'simple_jsonapi_rails.initialize' do
7
+ Mime::Type.register(SimpleJsonapi::MIME_TYPE, :jsonapi)
8
+
9
+ ActiveSupport.on_load(:action_controller) do
10
+ ::ActionDispatch::Request.parameter_parsers[:jsonapi] = ->(raw_post) do
11
+ ActiveSupport::JSON.decode(raw_post)
12
+ end
13
+
14
+ # In the renderers, `self` is the controller
15
+
16
+ ::ActionController::Renderers.add(:jsonapi_resource) do |resource, options|
17
+ self.content_type ||= Mime[:jsonapi]
18
+ SimpleJsonapi.render_resource(resource, options).to_json
19
+ end
20
+
21
+ ::ActionController::Renderers.add(:jsonapi_resources) do |resources, options|
22
+ self.content_type ||= Mime[:jsonapi]
23
+ SimpleJsonapi.render_resources(resources, options).to_json
24
+ end
25
+
26
+ ::ActionController::Renderers.add(:jsonapi_errors) do |errors, options|
27
+ self.content_type ||= Mime[:jsonapi]
28
+ SimpleJsonapi.render_errors(errors, options).to_json
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ # SimpleJsonapi::Rails::Railtie.initializers.each(&:run)
2
+
3
+ # Work-around for rack-test/rack-test#200. Remove once that issue is resolved.
4
+ module PatchRackTestDeleteRequests
5
+ def request(uri, env = {}, &block)
6
+ if env[:method] == :delete && env["HTTP_ACCEPT"] == SimpleJsonapi::MIME_TYPE && JSON.parse(env[:params]).present?
7
+ env[:input] = env[:params]
8
+ end
9
+
10
+ super(uri, env, &block)
11
+ end
12
+ end
13
+
14
+ Rack::Test::Session.prepend(PatchRackTestDeleteRequests)
15
+
16
+ ActiveSupport.on_load(:action_controller) do
17
+ ActionDispatch::IntegrationTest.register_encoder :jsonapi,
18
+ param_encoder: ->(params) { params.to_json },
19
+ response_parser: ->(body) { JSON.parse(body) }
20
+ end
@@ -0,0 +1,5 @@
1
+ module SimpleJsonapi
2
+ module Rails
3
+ VERSION = '1.0.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support'
2
+
3
+ require 'simple_jsonapi'
4
+
5
+ require 'simple_jsonapi/rails/extensions'
6
+ require 'simple_jsonapi/rails/extensions/routing'
7
+ require 'simple_jsonapi/rails/action_controller'
8
+ require 'simple_jsonapi/rails/action_controller/jsonapi_helper'
9
+ require 'simple_jsonapi/rails/railtie'
10
+
11
+ require 'simple_jsonapi/errors/active_record/record_not_found_serializer'
12
+ require 'simple_jsonapi/errors/active_model_error'
13
+ require 'simple_jsonapi/errors/active_model_error_serializer'
@@ -0,0 +1,4 @@
1
+ require 'simple_jsonapi/rails'
2
+
3
+ module SimpleJsonapi::Rails
4
+ end
@@ -0,0 +1,33 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'simple_jsonapi/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'simple_jsonapi_rails'
8
+ spec.version = SimpleJsonapi::Rails::VERSION
9
+ spec.license = "MIT"
10
+ spec.authors = ['PatientsLikeMe']
11
+ spec.email = ['engineers@patientslikeme.com']
12
+ spec.homepage = 'https://www.patientslikeme.com'
13
+
14
+ spec.summary = 'A library for integrating SimpleJsonapi into a Rails application.'
15
+ spec.description = 'A library for integrating SimpleJsonapi into a Rails application.'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'simple_jsonapi'
22
+ spec.add_runtime_dependency 'rails', '>= 4.2', '< 6.0'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.10'
25
+ spec.add_development_dependency 'listen'
26
+ spec.add_development_dependency 'minitest'
27
+ spec.add_development_dependency 'minitest-reporters'
28
+ spec.add_development_dependency 'pry'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'sqlite3'
31
+ spec.add_development_dependency 'will_paginate'
32
+ spec.add_development_dependency 'yard'
33
+ end
@@ -0,0 +1,299 @@
1
+ require_relative 'test_helper'
2
+
3
+ class ActionControllerTest < ActionDispatch::IntegrationTest
4
+ before do
5
+ @controller = OrdersController.new
6
+ @routes = Dummy::Application.routes
7
+ end
8
+
9
+ let(:order) { create_order }
10
+ let(:response_json) { response.parsed_body }
11
+
12
+ def create_order
13
+ Order.create!(customer_name: "Customer X", date: Date.today)
14
+ end
15
+
16
+ describe "ActionController" do
17
+ describe "basic get" do
18
+ it "gets a collection of resources without parameters" do
19
+ 2.times { create_order }
20
+
21
+ get orders_url, as: :jsonapi
22
+ assert_equal %w[data], response_json.keys
23
+ assert_equal(%w[orders orders], response_json["data"].map { |o| o["type"] })
24
+ end
25
+
26
+ it "gets a single resource without parameters" do
27
+ get order_path(order), as: :jsonapi
28
+ assert_equal %w[data], response_json.keys
29
+ assert_equal "orders", response_json.dig("data", "type")
30
+ assert_equal order.id.to_s, response_json.dig("data", "id")
31
+ end
32
+ end
33
+
34
+ describe "include parameter" do
35
+ it "splits a comma-delimited value" do
36
+ get orders_url(include: "this,that"), as: :jsonapi
37
+ assert_equal %w[this that], @controller.jsonapi.include_params
38
+ end
39
+
40
+ it "defaults to nil" do
41
+ get orders_url, as: :jsonapi
42
+ assert_nil @controller.jsonapi.include_params
43
+ end
44
+ end
45
+
46
+ describe "fields parameter" do
47
+ it "splits comma-delimited values" do
48
+ input = {
49
+ orders: "customer_name",
50
+ line_items: "product_name,quantity",
51
+ }
52
+ expected_output = {
53
+ "orders" => %w[customer_name],
54
+ "line_items" => %w[product_name quantity],
55
+ }
56
+
57
+ get orders_url(fields: input), as: :jsonapi
58
+
59
+ assert_equal expected_output, @controller.jsonapi.fields_params.to_unsafe_h
60
+ end
61
+
62
+ it "defaults to an empty hash" do
63
+ get orders_url, as: :jsonapi
64
+ assert_equal({}, @controller.jsonapi.fields_params)
65
+ end
66
+ end
67
+
68
+ describe "filter_param" do
69
+ it "fetches the parameter value" do
70
+ get orders_url("filter[this]" => "that"), as: :jsonapi
71
+
72
+ assert_equal "that", @controller.jsonapi.filter_param(:this)
73
+ end
74
+ end
75
+
76
+ describe "filter_param_list" do
77
+ it "splits a comma-delimited string" do
78
+ get orders_url("filter[these]" => "that,those"), as: :jsonapi
79
+
80
+ assert_equal %w[that those], @controller.jsonapi.filter_param_list(:these)
81
+ end
82
+
83
+ it "returns a simple string as an array" do
84
+ get orders_url("filter[this]" => "that"), as: :jsonapi
85
+
86
+ assert_equal %w[that], @controller.jsonapi.filter_param_list(:this)
87
+ end
88
+ end
89
+
90
+ describe "sort_related parameter" do
91
+ it "splits comma-delimited values" do
92
+ input = {
93
+ orders: "customer_name",
94
+ line_items: "product_name,quantity",
95
+ }
96
+ expected_output = {
97
+ "orders" => %w[customer_name],
98
+ "line_items" => %w[product_name quantity],
99
+ }
100
+
101
+ get orders_url(sort_related: input), as: :jsonapi
102
+
103
+ assert_equal expected_output, @controller.jsonapi.sort_related_params.to_unsafe_h
104
+ end
105
+
106
+ it "defaults to an empty hash" do
107
+ get orders_url, as: :jsonapi
108
+ assert_equal({}, @controller.jsonapi.fields_params)
109
+ end
110
+ end
111
+
112
+ describe "jsonapi_deserialize" do
113
+ let(:request_json) { request_hash.to_json }
114
+ let(:request_hash) do
115
+ {
116
+ data: {
117
+ type: "orders",
118
+ id: "1",
119
+ attributes: {
120
+ customer_name: "Jose",
121
+ date: "2017-10-01",
122
+ },
123
+ relationships: {
124
+ customer: {
125
+ data: { type: "customers", id: "11" },
126
+ },
127
+ products: {
128
+ data: [
129
+ { type: "products", id: "21" },
130
+ { type: "widgets", id: "22" },
131
+ ],
132
+ },
133
+ },
134
+ },
135
+ }
136
+ end
137
+
138
+ let(:helper) { SimpleJsonapi::Rails::ActionController::JsonapiHelper.new(nil) }
139
+ let(:deserialized) { helper.deserialize(request_hash) }
140
+
141
+ it "parses the request body" do
142
+ post orders_url, params: request_hash, as: :jsonapi
143
+ assert_response :created
144
+ assert_instance_of ActionController::Parameters, @controller.params[:order]
145
+ assert_equal "orders", @controller.params.dig(:order, :type)
146
+ assert_equal "1", @controller.params.dig(:order, :id)
147
+ end
148
+
149
+ it "is a no-op if there's no request body" do
150
+ 2.times { create_order }
151
+
152
+ get orders_url, as: :jsonapi
153
+ assert_response :ok
154
+ assert_nil @controller.params[:order]
155
+ end
156
+
157
+ it "moves the type and id to the object param" do
158
+ assert_equal "orders", deserialized[:type]
159
+ assert_equal "1", deserialized[:id]
160
+ end
161
+
162
+ it "moves the attributes to the object param" do
163
+ assert_equal "Jose", deserialized[:customer_name]
164
+ assert_equal "2017-10-01", deserialized[:date]
165
+ end
166
+
167
+ it "moves singular relationships to the object param" do
168
+ assert_equal "customers", deserialized[:customer_type]
169
+ assert_equal "11", deserialized[:customer_id]
170
+ end
171
+
172
+ it "moves collection relationships to the object param" do
173
+ assert_equal %w[products widgets], deserialized[:product_types]
174
+ assert_equal %w[21 22], deserialized[:product_ids]
175
+ end
176
+ end
177
+
178
+ describe "rendering errors" do
179
+ it "renders ActiveModel::Errors" do
180
+ post orders_url, params: {
181
+ data: {
182
+ type: "orders",
183
+ attributes: {
184
+ customer_name: "",
185
+ date: Date.today.iso8601,
186
+ },
187
+ },
188
+ }, as: :jsonapi
189
+
190
+ expected_output = {
191
+ errors: [{
192
+ status: "422",
193
+ code: "unprocessable_entity",
194
+ title: "Invalid customer_name",
195
+ detail: "Customer name can't be blank",
196
+ source: { pointer: "/data/attributes/customer_name" },
197
+ },],
198
+ }.deep_stringify_keys
199
+
200
+ assert_response :unprocessable_entity
201
+ assert_equal expected_output, response_json
202
+ end
203
+
204
+ it "renders ActiveRecord::RecordNotFound" do
205
+ get order_path(-1), as: :jsonapi
206
+
207
+ expected_output = {
208
+ errors: [{
209
+ status: "404",
210
+ code: "not_found",
211
+ title: "Not found",
212
+ detail: "Couldn't find Order with 'id'=-1",
213
+ source: { parameter: "id" },
214
+ },],
215
+ }.deep_stringify_keys
216
+
217
+ assert_response :not_found
218
+ assert_equal expected_output, response_json
219
+ end
220
+ end
221
+
222
+ describe "routing" do
223
+ it "generates correct relationship paths" do
224
+ assert_generates "/orders/1/relationships/items",
225
+ controller: "orders/relationships/items",
226
+ action: "add",
227
+ id: 1
228
+
229
+ assert_generates "/orders/1/relationships/items",
230
+ controller: "orders/relationships/items",
231
+ action: "remove",
232
+ id: 1
233
+
234
+ assert_generates "/orders/1/relationships/items",
235
+ controller: "orders/relationships/items",
236
+ action: "replace",
237
+ id: 1
238
+ end
239
+
240
+ it "generates the correct path helpers" do
241
+ assert_equal "/orders/1/relationships/items", orders_relationships_items_path(1)
242
+ end
243
+ end
244
+
245
+ describe "request validation" do
246
+ let(:request_hash) do
247
+ {
248
+ data: {
249
+ type: "orders",
250
+ attributes: {
251
+ customer_name: "Jose",
252
+ },
253
+ },
254
+ }
255
+ end
256
+
257
+ let(:jsonapi_mime_type) do
258
+ SimpleJsonapi::MIME_TYPE
259
+ end
260
+
261
+ it "returns a 406 if there is an accept header that does not match the required mime-type" do
262
+ get order_path(order), headers: { "Accept" => "application/json" }
263
+
264
+ assert_response :not_acceptable
265
+ end
266
+
267
+ # NB: This doesn't quite test what happens if there is no accept header, since Rails not so helpfully adds in an
268
+ # Accept header if none is present.
269
+ it "does not require an accept header" do
270
+ get order_path(order), headers: { "Accept" => "" }
271
+
272
+ assert_response :ok
273
+ end
274
+
275
+ it "returns a 415 if there is a request body but not the proper Content Type header" do
276
+ headers = {
277
+ "Accept" => jsonapi_mime_type,
278
+ "Content-Type" => "application/json",
279
+ }
280
+
281
+ post orders_url, params: request_hash.to_json, headers: headers
282
+
283
+ assert_response :unsupported_media_type
284
+ end
285
+
286
+ it "returns a 400 if there is no data element" do
287
+ post orders_url, params: { hinkle: "finkle_dinkle_doo" }, as: :jsonapi
288
+
289
+ assert_response :bad_request
290
+ end
291
+
292
+ it "returns a 400 if the request is to a relationship and there is not a data array" do
293
+ post orders_relationships_items_url(1), params: { data: "array non est" }, as: :jsonapi
294
+
295
+ assert_response :bad_request
296
+ end
297
+ end
298
+ end
299
+ end