jsonapi.rb 1.6.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc1e0239230a7a14a008b6344be8857fccd4db574af564a9b49342842fcc316d
4
- data.tar.gz: 16615a0a1e0bd8b6f0a84ae9f2adcc16005a16b6413577b3ee945fddb6ce90c5
3
+ metadata.gz: d531366279855044f83d35f4526a72156c711895290ab4dbce73f1c7615f1079
4
+ data.tar.gz: 63bd2be61a78d79a64bc739cbb492ee75a9e2f90a95389d55f133a2565627abb
5
5
  SHA512:
6
- metadata.gz: 2c65ee96100a9179205b0f8e220f4f42dc5cd0f03fb8c7c645cb1c70933f848e2e0fe11d600259f2001fe1a9d8760b821c4748009ee3451156e2b07a660df783
7
- data.tar.gz: 26c49d61c04f3bc03eaee260137905886a421c31d6bb669c063eb4cca9bea15a77327a07e90eb63ede9bb4b0d5e4b153aa38eb6dd6167c2eeabf10a1d7fb8fd6
6
+ metadata.gz: e602d6c993cd0259ac0b81c57eca16aee525f53ed0e5b4ac8014a0cb500528d579541375ce7735f0f191f3f732af9178ebbfdf7d4615eb967cfb244d1796cd36
7
+ data.tar.gz: 46a957ded4247be1607f6a4d84246da42a44fe41c58c55e2a34a914e958a28731531bc5e8a3f0d0141f571e17dce19c9c1f6f58845fc3e7db874d763dfcaffbb
data/README.md CHANGED
@@ -39,7 +39,7 @@ The available features include:
39
39
  [sparse fields](https://jsonapi.org/format/#fetching-sparse-fieldsets))
40
40
  * [filtering](https://jsonapi.org/format/#fetching-filtering) and
41
41
  [sorting](https://jsonapi.org/format/#fetching-sorting) of the data
42
- (powered by Ransack)
42
+ (powered by Ransack, soft-dependency)
43
43
  * [pagination](https://jsonapi.org/format/#fetching-pagination) support
44
44
 
45
45
  ## But how?
@@ -49,6 +49,16 @@ and [Ransack](https://github.com/activerecord-hackery/ransack).
49
49
 
50
50
  Thanks to everyone who worked on these amazing projects!
51
51
 
52
+ ## Sponsors
53
+
54
+ I'm grateful for the following companies for supporting this project!
55
+
56
+ <p align="center">
57
+ <a href="https://www.luneteyewear.com"><img src="https://user-images.githubusercontent.com/112147/136836142-2bfba96e-447f-4eb6-b137-2445aee81b37.png"/></a>
58
+ <a href="https://www.startuplandia.io"><img src="https://user-images.githubusercontent.com/112147/136836147-93f8ab17-2465-4477-a7ab-e38255483c66.png"/></a>
59
+ </p>
60
+
61
+
52
62
  ## Installation
53
63
 
54
64
  Add this line to your application's Gemfile:
@@ -231,6 +241,8 @@ to filter and sort over a collection of records.
231
241
  The support is pretty extended and covers also relationships and composite
232
242
  matchers.
233
243
 
244
+ Please add `ransack` to your `Gemfile` in order to benefit from this functionality!
245
+
234
246
  Here's an example:
235
247
 
236
248
  ```ruby
@@ -290,6 +302,7 @@ class MyController < ActionController::Base
290
302
  render jsonapi: paginated
291
303
  end
292
304
  end
305
+
293
306
  end
294
307
  ```
295
308
 
@@ -306,6 +319,17 @@ use the `jsonapi_pagination_meta` method:
306
319
  end
307
320
 
308
321
  ```
322
+
323
+ If you want to change the default number of items per page or define a custom logic to handle page size, use the
324
+ `jsonapi_page_size` method:
325
+
326
+ ```ruby
327
+ def jsonapi_page_size(pagination_params)
328
+ per_page = pagination_params[:size].to_f.to_i
329
+ per_page = 30 if per_page > 30 || per_page < 1
330
+ per_page
331
+ end
332
+ ```
309
333
  ### Deserialization
310
334
 
311
335
  `JSONAPI::Deserialization` provides a helper to transform a `JSONAPI` document
@@ -3,9 +3,6 @@ require 'jsonapi/error_serializer'
3
3
  module JSONAPI
4
4
  # [ActiveModel::Errors] serializer
5
5
  class ActiveModelErrorSerializer < ErrorSerializer
6
- set_id :object_id
7
- set_type :error
8
-
9
6
  attribute :status do
10
7
  '422'
11
8
  end
@@ -5,7 +5,6 @@ module JSONAPI
5
5
  class ErrorSerializer
6
6
  include JSONAPI::Serializer
7
7
 
8
- set_id :object_id
9
8
  set_type :error
10
9
 
11
10
  # Object/Hash attribute helpers.
@@ -15,6 +14,12 @@ module JSONAPI
15
14
  end
16
15
  end
17
16
 
17
+ # Overwrite the ID extraction method, to skip validations
18
+ #
19
+ # @return [NilClass]
20
+ def self.id_from_record(_record, _params)
21
+ end
22
+
18
23
  # Remap the root key to `errors`
19
24
  #
20
25
  # @return [Hash]
@@ -1,5 +1,9 @@
1
- require 'ransack/predicate'
2
- require_relative 'patches'
1
+ begin
2
+ require 'active_record'
3
+ require 'ransack'
4
+ require_relative 'patches'
5
+ rescue LoadError
6
+ end
3
7
 
4
8
  # Filtering and sorting support
5
9
  module JSONAPI
@@ -43,6 +43,8 @@ module JSONAPI
43
43
  original_url = request.base_url + request.path + '?'
44
44
 
45
45
  pagination.each do |page_name, number|
46
+ next if page_name == :records
47
+
46
48
  original_params[:page][:number] = number
47
49
  links[page_name] = original_url + CGI.unescape(
48
50
  original_params.to_query
@@ -63,7 +65,7 @@ module JSONAPI
63
65
  numbers = { current: page }
64
66
 
65
67
  if resources.respond_to?(:unscope)
66
- total = resources.unscope(:limit, :offset, :order).count()
68
+ total = resources.unscope(:limit, :offset, :order).size
67
69
  else
68
70
  # Try to fetch the cached size first
69
71
  total = resources.instance_variable_get(:@original_size)
@@ -82,6 +84,10 @@ module JSONAPI
82
84
  numbers[:last] = last_page
83
85
  end
84
86
 
87
+ if total.present?
88
+ numbers[:records] = total
89
+ end
90
+
85
91
  numbers
86
92
  end
87
93
 
@@ -89,16 +95,30 @@ module JSONAPI
89
95
  #
90
96
  # @return [Array] with the offset, limit and the current page number
91
97
  def jsonapi_pagination_params
92
- def_per_page = self.class.const_get(:JSONAPI_PAGE_SIZE).to_i
93
-
94
98
  pagination = params[:page].try(:slice, :number, :size) || {}
95
- per_page = pagination[:size].to_f.to_i
96
- per_page = def_per_page if per_page > def_per_page || per_page < 1
99
+ per_page = jsonapi_page_size(pagination)
97
100
  num = [1, pagination[:number].to_f.to_i].max
98
101
 
99
102
  [(num - 1) * per_page, per_page, num]
100
103
  end
101
104
 
105
+ # Retrieves the default page size
106
+ #
107
+ # @param per_page_param [Hash] opts the paginations params
108
+ # @option opts [String] :number the page number requested
109
+ # @option opts [String] :size the page size requested
110
+ #
111
+ # @return [Integer]
112
+ def jsonapi_page_size(pagination_params)
113
+ per_page = pagination_params[:size].to_f.to_i
114
+
115
+ return self.class
116
+ .const_get(:JSONAPI_PAGE_SIZE)
117
+ .to_i if per_page < 1
118
+
119
+ per_page
120
+ end
121
+
102
122
  # Fallback to Rack's parsed query string when Rails is not available
103
123
  #
104
124
  # @return [Hash]
data/lib/jsonapi/rails.rb CHANGED
@@ -37,7 +37,7 @@ module JSONAPI
37
37
  # @return [NilClass]
38
38
  def self.add_errors_renderer!
39
39
  ActionController::Renderers.add(:jsonapi_errors) do |resource, options|
40
- self.content_type ||= Mime[:jsonapi]
40
+ self.content_type = Mime[:jsonapi] if self.media_type.nil?
41
41
 
42
42
  many = JSONAPI::Rails.is_collection?(resource, options[:is_collection])
43
43
  resource = [resource] unless many
@@ -47,7 +47,7 @@ module JSONAPI
47
47
  ) unless resource.is_a?(ActiveModel::Errors)
48
48
 
49
49
  errors = []
50
- model = resource.instance_variable_get('@base')
50
+ model = resource.instance_variable_get(:@base)
51
51
 
52
52
  if respond_to?(:jsonapi_serializer_class, true)
53
53
  model_serializer = jsonapi_serializer_class(model, false)
@@ -55,8 +55,18 @@ module JSONAPI
55
55
  model_serializer = JSONAPI::Rails.serializer_class(model, false)
56
56
  end
57
57
 
58
- details = resource.messages
59
- details = resource.details if resource.respond_to?(:details)
58
+ details = {}
59
+ if ::Rails.gem_version >= Gem::Version.new('6.1')
60
+ resource.each do |error|
61
+ attr = error.attribute
62
+ details[attr] ||= []
63
+ details[attr] << error.detail.merge(message: error.message)
64
+ end
65
+ elsif resource.respond_to?(:details)
66
+ details = resource.details
67
+ else
68
+ details = resource.messages
69
+ end
60
70
 
61
71
  details.each do |error_key, error_hashes|
62
72
  error_hashes.each do |error_hash|
@@ -80,7 +90,7 @@ module JSONAPI
80
90
  # @return [NilClass]
81
91
  def self.add_renderer!
82
92
  ActionController::Renderers.add(:jsonapi) do |resource, options|
83
- self.content_type ||= Mime[:jsonapi]
93
+ self.content_type = Mime[:jsonapi] if self.media_type.nil?
84
94
 
85
95
  JSONAPI_METHODS_MAPPING.to_a[0..1].each do |opt, method_name|
86
96
  next unless respond_to?(method_name, true)
@@ -90,7 +100,7 @@ module JSONAPI
90
100
  # If it's an empty collection, return it directly.
91
101
  many = JSONAPI::Rails.is_collection?(resource, options[:is_collection])
92
102
  if many && !resource.any?
93
- return options.slice(:meta, :links).merge(data: []).to_json
103
+ return options.slice(:meta, :links).compact.merge(data: []).to_json
94
104
  end
95
105
 
96
106
  JSONAPI_METHODS_MAPPING.to_a[2..-1].each do |opt, method_name|
@@ -111,15 +121,13 @@ module JSONAPI
111
121
 
112
122
  # Checks if an object is a collection
113
123
  #
114
- # Stolen from [JSONAPI::Serializer], instance method.
124
+ # Basically forwards it to a [JSONAPI::Serializer] as there's no public API
115
125
  #
116
126
  # @param resource [Object] to check
117
127
  # @param force_is_collection [NilClass] flag to overwrite
118
128
  # @return [TrueClass] upon success
119
129
  def self.is_collection?(resource, force_is_collection = nil)
120
- return force_is_collection unless force_is_collection.nil?
121
-
122
- resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
130
+ JSONAPI::ErrorSerializer.is_collection?(resource, force_is_collection)
123
131
  end
124
132
 
125
133
  # Resolves resource serializer class
@@ -1,3 +1,3 @@
1
1
  module JSONAPI
2
- VERSION = '1.6.0'
2
+ VERSION = '2.0.1'
3
3
  end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe JSONAPI::Deserialization do
4
+ let(:jsonapi_deserialize) { UsersController.new.method(:jsonapi_deserialize) }
5
+ let(:document) do
6
+ {
7
+ data: {
8
+ id: 1,
9
+ type: 'note',
10
+ attributes: {
11
+ title: 'Title 1',
12
+ date: '2015-12-20'
13
+ },
14
+ relationships: {
15
+ author: {
16
+ data: {
17
+ type: 'user',
18
+ id: 2
19
+ }
20
+ },
21
+ second_author: {
22
+ data: nil
23
+ },
24
+ notes: {
25
+ data: [
26
+ {
27
+ type: 'note',
28
+ id: 3
29
+ },
30
+ {
31
+ type: 'note',
32
+ id: 4
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ }
38
+ }
39
+ end
40
+
41
+ describe '#jsonapi_deserialize' do
42
+ it do
43
+ expect(jsonapi_deserialize.call(document)).to eq(
44
+ 'id' => 1,
45
+ 'date' => '2015-12-20',
46
+ 'title' => 'Title 1',
47
+ 'author_id' => 2,
48
+ 'second_author_id' => nil,
49
+ 'note_ids' => [3, 4]
50
+ )
51
+ end
52
+
53
+ context 'with `only`' do
54
+ it do
55
+ expect(jsonapi_deserialize.call(document, only: :notes)).to eq(
56
+ 'note_ids' => [3, 4]
57
+ )
58
+ end
59
+ end
60
+
61
+ context 'with `except`' do
62
+ it do
63
+ expect(
64
+ jsonapi_deserialize.call(document, except: [:date, :title])
65
+ ).to eq(
66
+ 'id' => 1,
67
+ 'author_id' => 2,
68
+ 'second_author_id' => nil,
69
+ 'note_ids' => [3, 4]
70
+ )
71
+ end
72
+ end
73
+
74
+ context 'with `polymorphic`' do
75
+ it do
76
+ expect(
77
+ jsonapi_deserialize.call(
78
+ document, only: :author, polymorphic: :author
79
+ )
80
+ ).to eq(
81
+ 'author_id' => 2,
82
+ 'author_type' => User.name
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
data/spec/dummy.rb ADDED
@@ -0,0 +1,162 @@
1
+ require 'securerandom'
2
+ require 'rails/all'
3
+ require 'ransack'
4
+ require 'jsonapi'
5
+
6
+ Rails.logger = Logger.new(STDOUT)
7
+ Rails.logger.level = ENV['LOG_LEVEL'] || Logger::WARN
8
+
9
+ JSONAPI::Rails.install!
10
+
11
+ ActiveRecord::Base.logger = Rails.logger
12
+ ActiveRecord::Base.establish_connection(
13
+ ENV['DATABASE_URL'] || 'sqlite3::memory:'
14
+ )
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :users, force: true do |t|
18
+ t.string :first_name
19
+ t.string :last_name
20
+ t.timestamps
21
+ end
22
+
23
+ create_table :notes, force: true do |t|
24
+ t.string :title
25
+ t.integer :user_id
26
+ t.integer :quantity
27
+ t.timestamps
28
+ end
29
+ end
30
+
31
+ class User < ActiveRecord::Base
32
+ has_many :notes
33
+ end
34
+
35
+ class Note < ActiveRecord::Base
36
+ validates_format_of :title, without: /BAD_TITLE/
37
+ validates_numericality_of :quantity, less_than: 100, if: :quantity?
38
+ belongs_to :user, required: true
39
+ end
40
+
41
+ class CustomNoteSerializer
42
+ include JSONAPI::Serializer
43
+
44
+ set_type :note
45
+ belongs_to :user
46
+ attributes(:title, :quantity, :created_at, :updated_at)
47
+ end
48
+
49
+ class UserSerializer
50
+ include JSONAPI::Serializer
51
+
52
+ has_many :notes, serializer: CustomNoteSerializer
53
+ attributes(:last_name, :created_at, :updated_at)
54
+
55
+ attribute :first_name do |object, params|
56
+ if params[:first_name_upcase]
57
+ object.first_name.upcase
58
+ else
59
+ object.first_name
60
+ end
61
+ end
62
+ end
63
+
64
+ class Dummy < Rails::Application
65
+ secrets.secret_key_base = '_'
66
+ config.hosts << 'www.example.com' if config.respond_to?(:hosts)
67
+
68
+ routes.draw do
69
+ scope defaults: { format: :jsonapi } do
70
+ resources :users, only: [:index]
71
+ resources :notes, only: [:update]
72
+ end
73
+ end
74
+ end
75
+
76
+ class UsersController < ActionController::Base
77
+ include JSONAPI::Fetching
78
+ include JSONAPI::Filtering
79
+ include JSONAPI::Pagination
80
+ include JSONAPI::Deserialization
81
+
82
+ def index
83
+ allowed_fields = [
84
+ :first_name, :last_name, :created_at,
85
+ :notes_created_at, :notes_quantity
86
+ ]
87
+ options = { sort_with_expressions: true }
88
+
89
+ jsonapi_filter(User.all, allowed_fields, options) do |filtered|
90
+ result = filtered.result
91
+
92
+ if params[:sort].to_s.include?('notes_quantity')
93
+ render jsonapi: result.group('id').to_a
94
+ return
95
+ end
96
+
97
+ result = result.to_a if params[:as_list]
98
+
99
+ jsonapi_paginate(result) do |paginated|
100
+ render jsonapi: paginated
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+ def jsonapi_meta(resources)
107
+ {
108
+ many: true,
109
+ pagination: jsonapi_pagination_meta(resources)
110
+ }
111
+ end
112
+
113
+ def jsonapi_serializer_params
114
+ {
115
+ first_name_upcase: params[:upcase]
116
+ }
117
+ end
118
+ end
119
+
120
+ class NotesController < ActionController::Base
121
+ include JSONAPI::Errors
122
+ include JSONAPI::Deserialization
123
+
124
+ def update
125
+ raise_error! if params[:id] == 'tada'
126
+
127
+ note = Note.find(params[:id])
128
+
129
+ if note.update(note_params)
130
+ render jsonapi: note
131
+ else
132
+ note.errors.add(:title, message: 'has typos') if note.errors.key?(:title)
133
+
134
+ render jsonapi_errors: note.errors, status: :unprocessable_entity
135
+ end
136
+ end
137
+
138
+ private
139
+ def render_jsonapi_internal_server_error(exception)
140
+ Rails.logger.error(exception)
141
+ super(exception)
142
+ end
143
+
144
+ def jsonapi_serializer_class(resource, is_collection)
145
+ JSONAPI::Rails.serializer_class(resource, is_collection)
146
+ rescue NameError
147
+ klass = resource.class
148
+ klass = resource.first.class if is_collection
149
+ "Custom#{klass.name}Serializer".constantize
150
+ end
151
+
152
+ def note_params
153
+ # Will trigger required attribute error handling
154
+ params.require(:data).require(:attributes).require(:title)
155
+
156
+ jsonapi_deserialize(params)
157
+ end
158
+
159
+ def jsonapi_meta(resources)
160
+ { single: true }
161
+ end
162
+ end
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NotesController, type: :request do
4
+ describe 'PUT /notes/:id' do
5
+ let(:note) { create_note }
6
+ let(:note_id) { note.id }
7
+ let(:user) { note.user }
8
+ let(:user_id) { user.id }
9
+ let(:note_params) do
10
+ {
11
+ data: {
12
+ attributes: { title: FFaker::Company.name },
13
+ relationships: { user: { data: { id: user_id } } }
14
+ }
15
+ }
16
+ end
17
+ let(:params) { note_params }
18
+
19
+ before do
20
+ put(note_path(note_id), params: params.to_json, headers: jsonapi_headers)
21
+ end
22
+
23
+ it do
24
+ expect(response).to have_http_status(:ok)
25
+ expect(response_json['data']).to have_id(note.id.to_s)
26
+ expect(response_json['meta']).to eq('single' => true)
27
+ end
28
+
29
+ context 'with a missing parameter in the payload' do
30
+ let(:params) { {} }
31
+
32
+ it do
33
+ expect(response).to have_http_status(:unprocessable_entity)
34
+ expect(response_json['errors'].size).to eq(1)
35
+ expect(response_json['errors'][0]['status']).to eq('422')
36
+ expect(response_json['errors'][0]['title'])
37
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
38
+ expect(response_json['errors'][0]['source']).to eq('pointer' => '')
39
+ expect(response_json['errors'][0]['detail']).to be_nil
40
+ end
41
+ end
42
+
43
+ context 'with an invalid payload' do
44
+ let(:params) do
45
+ payload = note_params.dup
46
+ payload[:data][:relationships][:user][:data][:id] = nil
47
+ payload
48
+ end
49
+
50
+ it do
51
+ expect(response).to have_http_status(:unprocessable_entity)
52
+ expect(response_json['errors'].size).to eq(1)
53
+ expect(response_json['errors'][0]['status']).to eq('422')
54
+ expect(response_json['errors'][0]['code']).to include('blank')
55
+ expect(response_json['errors'][0]['title'])
56
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
57
+ expect(response_json['errors'][0]['source'])
58
+ .to eq('pointer' => '/data/relationships/user')
59
+ if Rails.gem_version >= Gem::Version.new('6.1')
60
+ expect(response_json['errors'][0]['detail'])
61
+ .to eq('User must exist')
62
+ else
63
+ expect(response_json['errors'][0]['detail'])
64
+ .to eq('User can\'t be blank')
65
+ end
66
+ end
67
+
68
+ context 'required by validations' do
69
+ let(:params) do
70
+ payload = note_params.dup
71
+ payload[:data][:attributes][:title] = 'BAD_TITLE'
72
+ payload[:data][:attributes][:quantity] = 100 + rand(10)
73
+ payload
74
+ end
75
+
76
+ it do
77
+ expect(response).to have_http_status(:unprocessable_entity)
78
+ expect(response_json['errors'].size).to eq(3)
79
+ expect(response_json['errors'][0]['status']).to eq('422')
80
+ expect(response_json['errors'][0]['code']).to include('invalid')
81
+ expect(response_json['errors'][0]['title'])
82
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
83
+ expect(response_json['errors'][0]['source'])
84
+ .to eq('pointer' => '/data/attributes/title')
85
+ expect(response_json['errors'][0]['detail'])
86
+ .to eq('Title is invalid')
87
+
88
+ expect(response_json['errors'][1]['status']).to eq('422')
89
+
90
+ if Rails::VERSION::MAJOR >= 5
91
+ expect(response_json['errors'][1]['code']).to eq('invalid')
92
+ else
93
+ expect(response_json['errors'][1]['code']).to eq('has_typos')
94
+ end
95
+
96
+ expect(response_json['errors'][1]['title'])
97
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
98
+ expect(response_json['errors'][1]['source'])
99
+ .to eq('pointer' => '/data/attributes/title')
100
+ expect(response_json['errors'][1]['detail'])
101
+ .to eq('Title has typos')
102
+
103
+ expect(response_json['errors'][2]['status']).to eq('422')
104
+
105
+ if Rails::VERSION::MAJOR >= 5
106
+ expect(response_json['errors'][2]['code']).to eq('less_than')
107
+ else
108
+ expect(response_json['errors'][2]['code'])
109
+ .to eq('must_be_less_than_100')
110
+ end
111
+
112
+ expect(response_json['errors'][2]['title'])
113
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[422])
114
+ expect(response_json['errors'][2]['source'])
115
+ .to eq('pointer' => '/data/attributes/quantity')
116
+ expect(response_json['errors'][2]['detail'])
117
+ .to eq('Quantity must be less than 100')
118
+ end
119
+ end
120
+
121
+ context 'as a param attribute' do
122
+ let(:params) do
123
+ payload = note_params.dup
124
+ payload[:data][:attributes].delete(:title)
125
+ # To have any attribtues in the payload...
126
+ payload[:data][:attributes][:created_at] = nil
127
+ payload
128
+ end
129
+
130
+ it do
131
+ expect(response).to have_http_status(:unprocessable_entity)
132
+ expect(response_json['errors'][0]['source'])
133
+ .to eq('pointer' => '/data/attributes/title')
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'with a bad note ID' do
139
+ let(:user_id) { nil }
140
+ let(:note_id) { rand(10) }
141
+
142
+ it do
143
+ expect(response).to have_http_status(:not_found)
144
+ expect(response_json['errors'].size).to eq(1)
145
+ expect(response_json['errors'][0]['status']).to eq('404')
146
+ expect(response_json['errors'][0]['title'])
147
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[404])
148
+ expect(response_json['errors'][0]['source']).to be_nil
149
+ expect(response_json['errors'][0]['detail']).to be_nil
150
+ end
151
+ end
152
+
153
+ context 'with an exception' do
154
+ let(:user_id) { nil }
155
+ let(:note_id) { 'tada' }
156
+
157
+ it do
158
+ expect(response).to have_http_status(:internal_server_error)
159
+ expect(response_json['errors'].size).to eq(1)
160
+ expect(response_json['errors'][0]['status']).to eq('500')
161
+ expect(response_json['errors'][0]['title'])
162
+ .to eq(Rack::Utils::HTTP_STATUS_CODES[500])
163
+ expect(response_json['errors'][0]['source']).to be_nil
164
+ expect(response_json['errors'][0]['detail']).to be_nil
165
+ end
166
+ end
167
+ end
168
+ end