simple_jsonapi_rails 1.0.0

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