jsonapi.rb 1.5.7 → 2.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.
@@ -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
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe UsersController, type: :request do
4
+ describe 'GET /users' do
5
+ let!(:user) { }
6
+ let(:params) { }
7
+
8
+ before do
9
+ get(users_path, params: params, headers: jsonapi_headers)
10
+ end
11
+
12
+ context 'with users' do
13
+ let(:first_user) { create_user }
14
+ let(:second_user) { create_user }
15
+ let(:third_user) { create_note.user }
16
+ let(:users) { [first_user, second_user, third_user] }
17
+ let(:user) { users.last }
18
+ let(:note) { third_user.notes.first }
19
+
20
+ context 'returns customers and dasherized first name' do
21
+ let(:params) do
22
+ { upcase: :yes, fields: { unknown: nil } }
23
+ end
24
+
25
+ it do
26
+ expect(response).to have_http_status(:ok)
27
+ expect(response_json['data'].size).to eq(users.size)
28
+
29
+ response_json['data'].each do |item|
30
+ user = users.detect { |u| u.id == item['id'].to_i }
31
+ expect(item).to have_attribute('first_name')
32
+ .with_value(user.first_name.upcase)
33
+ end
34
+ end
35
+ end
36
+
37
+ context 'returns customers included and sparse fields' do
38
+ let(:params) do
39
+ {
40
+ include: 'notes',
41
+ fields: { note: 'title,updated_at' }
42
+ }
43
+ end
44
+
45
+ it do
46
+ expect(response).to have_http_status(:ok)
47
+ expect(response_json['data'].last)
48
+ .to have_relationship(:notes)
49
+ .with_data([
50
+ { 'id' => note.id.to_s, 'type' => 'note' }
51
+ ])
52
+ expect(response_json['included']).to include(
53
+ 'id' => note.id.to_s,
54
+ 'type' => 'note',
55
+ 'relationships' => {},
56
+ 'attributes' => {
57
+ 'title' => note.title,
58
+ 'updated_at' => note.updated_at.as_json
59
+ }
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe UsersController, type: :request do
4
+ describe '#extract_attributes_and_predicate' do
5
+ context 'mixed attributes (and/or)' do
6
+ it 'extracts ANDs' do
7
+ attributes, predicates = JSONAPI::Filtering
8
+ .extract_attributes_and_predicates('attr1_and_attr2_eq')
9
+ expect(attributes).to eq(['attr1', 'attr2'])
10
+ expect(predicates.size).to eq(1)
11
+ expect(predicates[0].name).to eq('eq')
12
+ end
13
+ end
14
+
15
+ context 'mixed predicates' do
16
+ it 'extracts in order' do
17
+ attributes, predicates = JSONAPI::Filtering
18
+ .extract_attributes_and_predicates('attr1_sum_eq')
19
+ expect(attributes).to eq(['attr1'])
20
+ expect(predicates.size).to eq(2)
21
+ expect(predicates[0].name).to eq('sum')
22
+ expect(predicates[1].name).to eq('eq')
23
+ end
24
+ end
25
+ end
26
+
27
+ describe 'GET /users' do
28
+ let!(:user) { }
29
+ let(:params) { }
30
+
31
+ before do
32
+ get(users_path, params: params, headers: jsonapi_headers)
33
+ end
34
+
35
+ context 'with users' do
36
+ let(:first_user) { create_user }
37
+ let(:second_user) { create_note.user }
38
+ let(:third_user) { create_user }
39
+ let(:users) { [first_user, second_user, third_user] }
40
+ let(:user) { users.last }
41
+
42
+ context 'returns filtered users' do
43
+ let(:params) do
44
+ {
45
+ filter: {
46
+ last_name_or_first_name_cont_any: (
47
+ "#{third_user.first_name[0..5]}%,#{self.class.name}"
48
+ )
49
+ }
50
+ }
51
+ end
52
+
53
+ it do
54
+ expect(response).to have_http_status(:ok)
55
+ expect(response_json['data'].size).to eq(1)
56
+ expect(response_json['data'][0]).to have_id(third_user.id.to_s)
57
+ end
58
+
59
+ context 'with a comma' do
60
+ let(:params) do
61
+ third_user.update(first_name: third_user.first_name + ',')
62
+
63
+ {
64
+ filter: { first_name_eq: third_user.first_name }
65
+ }
66
+ end
67
+
68
+ it do
69
+ expect(response).to have_http_status(:ok)
70
+ expect(response_json['data'].size).to eq(1)
71
+ expect(response_json['data'][0]).to have_id(third_user.id.to_s)
72
+ end
73
+ end
74
+ end
75
+
76
+ context 'returns sorted users by notes quantity sum' do
77
+ let(:params) do
78
+ { sort: '-notes_quantity_sum' }
79
+ end
80
+
81
+ it do
82
+ expect(response).to have_http_status(:ok)
83
+ expect(response_json['data'].size).to eq(3)
84
+ expect(response_json['data'][0]).to have_id(second_user.id.to_s)
85
+ end
86
+ end
87
+
88
+ context 'returns sorted users by notes' do
89
+ let(:params) do
90
+ { sort: '-notes_created_at' }
91
+ end
92
+
93
+ it do
94
+ expect(response).to have_http_status(:ok)
95
+ expect(response_json['data'].size).to eq(3)
96
+ expect(response_json['data'][0]).to have_id(second_user.id.to_s)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,246 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe UsersController, type: :request do
4
+ describe 'GET /users' do
5
+ let!(:user) { }
6
+ let(:params) do
7
+ {
8
+ page: { number: 'Nan', size: 'NaN' },
9
+ sort: '-created_at'
10
+ }
11
+ end
12
+
13
+ before do
14
+ get(users_path, params: params, headers: jsonapi_headers)
15
+ end
16
+
17
+ it do
18
+ expect(response_json['data'].size).to eq(0)
19
+ expect(response_json['meta'])
20
+ .to eq(
21
+ 'many' => true,
22
+ 'pagination' => {
23
+ 'current' => 1,
24
+ 'records' => 0
25
+ }
26
+ )
27
+ end
28
+
29
+ context 'with users' do
30
+ let(:first_user) { create_user }
31
+ let(:second_user) { create_user }
32
+ let(:third_user) { create_user }
33
+ let(:users) { [first_user, second_user, third_user] }
34
+ let(:user) { users.last }
35
+
36
+ context 'returns users with pagination links' do
37
+ it do
38
+ expect(response).to have_http_status(:ok)
39
+ expect(response_json['data'].size).to eq(3)
40
+ expect(response_json['data'][0]).to have_id(third_user.id.to_s)
41
+ expect(response_json['data'][1]).to have_id(second_user.id.to_s)
42
+ expect(response_json['data'][2]).to have_id(first_user.id.to_s)
43
+
44
+ expect(response_json).to have_link('self')
45
+ expect(response_json).not_to have_link(:prev)
46
+ expect(response_json).not_to have_link(:next)
47
+ expect(response_json).not_to have_link(:first)
48
+ expect(response_json).not_to have_link(:last)
49
+
50
+ expect(URI.parse(response_json['links']['self']).query)
51
+ .to eq(CGI.unescape(params.to_query))
52
+ end
53
+
54
+ context 'on page 2 out of 3' do
55
+ let(:as_list) { }
56
+ let(:params) do
57
+ {
58
+ page: { number: 2, size: 1 },
59
+ sort: '-created_at',
60
+ as_list: as_list
61
+ }.reject { |_k, _v| _v.blank? }
62
+ end
63
+
64
+ context 'on an array of resources' do
65
+ let(:as_list) { true }
66
+
67
+ it do
68
+ expect(response).to have_http_status(:ok)
69
+ expect(response_json['data'].size).to eq(1)
70
+ expect(response_json['data'][0]).to have_id(second_user.id.to_s)
71
+
72
+ expect(response_json['meta']['pagination']).to eq(
73
+ 'current' => 2,
74
+ 'first' => 1,
75
+ 'prev' => 1,
76
+ 'next' => 3,
77
+ 'last' => 3,
78
+ 'records' => 3
79
+ )
80
+ end
81
+ end
82
+
83
+ it do
84
+ expect(response).to have_http_status(:ok)
85
+ expect(response_json['data'].size).to eq(1)
86
+ expect(response_json['data'][0]).to have_id(second_user.id.to_s)
87
+
88
+ expect(response_json['meta']['pagination']).to eq(
89
+ 'current' => 2,
90
+ 'first' => 1,
91
+ 'prev' => 1,
92
+ 'next' => 3,
93
+ 'last' => 3,
94
+ 'records' => 3
95
+ )
96
+
97
+ expect(response_json).to have_link(:self)
98
+ expect(response_json).to have_link(:prev)
99
+ expect(response_json).to have_link(:first)
100
+ expect(response_json).to have_link(:next)
101
+ expect(response_json).to have_link(:last)
102
+
103
+ qry = CGI.unescape(params.to_query)
104
+ expect(URI.parse(response_json['links']['self']).query).to eq(qry)
105
+
106
+ qry = CGI.unescape(params.deep_merge(page: { number: 2 }).to_query)
107
+ expect(URI.parse(response_json['links']['self']).query).to eq(qry)
108
+
109
+ qry = CGI.unescape(params.deep_merge(page: { number: 1 }).to_query)
110
+ expect(URI.parse(response_json['links']['prev']).query).to eq(qry)
111
+ expect(URI.parse(response_json['links']['first']).query).to eq(qry)
112
+
113
+ qry = CGI.unescape(params.deep_merge(page: { number: 3 }).to_query)
114
+ expect(URI.parse(response_json['links']['next']).query).to eq(qry)
115
+ expect(URI.parse(response_json['links']['last']).query).to eq(qry)
116
+ end
117
+ end
118
+
119
+ context 'on page 3 out of 3' do
120
+ let(:params) do
121
+ {
122
+ page: { number: 3, size: 1 }
123
+ }
124
+ end
125
+
126
+ it do
127
+ expect(response).to have_http_status(:ok)
128
+ expect(response_json['data'].size).to eq(1)
129
+
130
+ expect(response_json['meta']['pagination']).to eq(
131
+ 'current' => 3,
132
+ 'first' => 1,
133
+ 'prev' => 2,
134
+ 'records' => 3
135
+ )
136
+
137
+ expect(response_json).to have_link(:self)
138
+ expect(response_json).to have_link(:prev)
139
+ expect(response_json).to have_link(:first)
140
+ expect(response_json).not_to have_link(:next)
141
+ expect(response_json).not_to have_link(:last)
142
+
143
+ expect(URI.parse(response_json['links']['self']).query)
144
+ .to eq(CGI.unescape(params.to_query))
145
+
146
+ qry = CGI.unescape(params.deep_merge(page: { number: 2 }).to_query)
147
+ expect(URI.parse(response_json['links']['prev']).query).to eq(qry)
148
+
149
+ qry = CGI.unescape(params.deep_merge(page: { number: 1 }).to_query)
150
+ expect(URI.parse(response_json['links']['first']).query).to eq(qry)
151
+ end
152
+ end
153
+
154
+ context 'on paging beyond the last page' do
155
+ let(:as_list) { }
156
+ let(:params) do
157
+ {
158
+ page: { number: 5, size: 1 },
159
+ as_list: as_list
160
+ }.reject { |_k, _v| _v.blank? }
161
+ end
162
+
163
+ context 'on an array of resources' do
164
+ let(:as_list) { true }
165
+
166
+ it do
167
+ expect(response).to have_http_status(:ok)
168
+ expect(response_json['data'].size).to eq(0)
169
+
170
+ expect(response_json['meta']['pagination']).to eq(
171
+ 'current' => 5,
172
+ 'first' => 1,
173
+ 'prev' => 4,
174
+ 'records' => 3
175
+ )
176
+ end
177
+ end
178
+
179
+ it do
180
+ expect(response).to have_http_status(:ok)
181
+ expect(response_json['data'].size).to eq(0)
182
+
183
+ expect(response_json['meta']['pagination']).to eq(
184
+ 'current' => 5,
185
+ 'first' => 1,
186
+ 'prev' => 4,
187
+ 'records' => 3
188
+ )
189
+
190
+ expect(response_json).to have_link(:self)
191
+ expect(response_json).to have_link(:prev)
192
+ expect(response_json).to have_link(:first)
193
+ expect(response_json).not_to have_link(:next)
194
+ expect(response_json).not_to have_link(:last)
195
+
196
+ expect(URI.parse(response_json['links']['self']).query)
197
+ .to eq(CGI.unescape(params.to_query))
198
+
199
+ qry = CGI.unescape(params.deep_merge(page: { number: 4 }).to_query)
200
+ expect(URI.parse(response_json['links']['prev']).query).to eq(qry)
201
+
202
+ qry = CGI.unescape(params.deep_merge(page: { number: 1 }).to_query)
203
+ expect(URI.parse(response_json['links']['first']).query).to eq(qry)
204
+ end
205
+ end
206
+
207
+ context 'on page 1 out of 3' do
208
+ let(:params) do
209
+ {
210
+ page: { size: 1 },
211
+ sort: '-created_at'
212
+ }
213
+ end
214
+
215
+ it do
216
+ expect(response).to have_http_status(:ok)
217
+ expect(response_json['data'].size).to eq(1)
218
+ expect(response_json['data'][0]).to have_id(third_user.id.to_s)
219
+
220
+ expect(response_json['meta']['pagination']).to eq(
221
+ 'current' => 1,
222
+ 'next' => 2,
223
+ 'last' => 3,
224
+ 'records' => 3
225
+ )
226
+
227
+ expect(response_json).not_to have_link(:prev)
228
+ expect(response_json).not_to have_link(:first)
229
+ expect(response_json).to have_link(:next)
230
+ expect(response_json).to have_link(:self)
231
+ expect(response_json).to have_link(:last)
232
+
233
+ expect(URI.parse(response_json['links']['self']).query)
234
+ .to eq(CGI.unescape(params.to_query))
235
+
236
+ qry = CGI.unescape(params.deep_merge(page: { number: 2 }).to_query)
237
+ expect(URI.parse(response_json['links']['next']).query).to eq(qry)
238
+
239
+ qry = CGI.unescape(params.deep_merge(page: { number: 3 }).to_query)
240
+ expect(URI.parse(response_json['links']['last']).query).to eq(qry)
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,87 @@
1
+ require 'bundler/setup'
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start do
5
+ add_group 'Lib', 'lib'
6
+ add_group 'Tests', 'spec'
7
+ end
8
+ SimpleCov.minimum_coverage 90
9
+
10
+ require 'dummy'
11
+ require 'ffaker'
12
+ require 'rspec/rails'
13
+ require 'jsonapi/rspec'
14
+
15
+ RSpec.configure do |config|
16
+ config.include JSONAPI::RSpec
17
+
18
+ config.use_transactional_fixtures = true
19
+ config.mock_with :rspec
20
+ config.filter_run_when_matching :focus
21
+ config.disable_monkey_patching!
22
+
23
+ config.expect_with :rspec do |c|
24
+ c.syntax = :expect
25
+ end
26
+ end
27
+
28
+ module RSpecHelpers
29
+ include Dummy.routes.url_helpers
30
+
31
+ # Helper to return JSONAPI valid headers
32
+ #
33
+ # @return [Hash] the relevant content type &co
34
+ def jsonapi_headers
35
+ { 'Content-Type': Mime[:jsonapi].to_s }
36
+ end
37
+
38
+ # Parses and returns a deserialized JSON
39
+ #
40
+ # @return [Hash]
41
+ def response_json
42
+ JSON.parse(response.body)
43
+ end
44
+
45
+ # Creates an user
46
+ #
47
+ # @return [User]
48
+ def create_user
49
+ User.create!(
50
+ first_name: FFaker::Name.first_name,
51
+ last_name: FFaker::Name.last_name
52
+ )
53
+ end
54
+
55
+ # Creates a note
56
+ #
57
+ # @return [Note]
58
+ def create_note(user = nil)
59
+ Note.create!(
60
+ title: FFaker::Company.name,
61
+ quantity: rand(10),
62
+ user: (user || create_user)
63
+ )
64
+ end
65
+ end
66
+
67
+ module Rails4RequestMethods
68
+ [:get, :post, :put, :delete].each do |method_name|
69
+ define_method(method_name) do |path, named_args|
70
+ super(
71
+ path,
72
+ named_args.delete(:params),
73
+ named_args.delete(:headers)
74
+ )
75
+ end
76
+ end
77
+ end
78
+
79
+ RSpec.configure do |config|
80
+ config.include RSpecHelpers, type: :request
81
+ config.include RSpecHelpers, type: :controller
82
+
83
+ if ::Rails::VERSION::MAJOR == 4
84
+ config.include Rails4RequestMethods, type: :request
85
+ config.include Rails4RequestMethods, type: :controller
86
+ end
87
+ end