jsonapi.rb 1.6.0 → 2.0.1

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.
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