infinum_json_api_setup 0.0.7 → 0.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +45 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +50 -12
- data/Gemfile +1 -0
- data/Gemfile.lock +1 -0
- data/README.md +8 -0
- data/Rakefile +3 -0
- data/base.gemfile +14 -0
- data/infinum_json_api_setup.gemspec +11 -16
- data/lib/generators/infinum_json_api_setup/templates/config/locales/json_api.en.yml +1 -0
- data/lib/infinum_json_api_setup/json_api/content_negotiation.rb +2 -2
- data/lib/infinum_json_api_setup/json_api/locale_negotiation.rb +45 -0
- data/lib/infinum_json_api_setup/json_api/serializer_options.rb +1 -1
- data/lib/infinum_json_api_setup/rspec/helpers/response_helper.rb +4 -4
- data/lib/infinum_json_api_setup/rspec/matchers/have_empty_data.rb +1 -1
- data/lib/infinum_json_api_setup/rspec/matchers/have_error_pointer.rb +1 -1
- data/lib/infinum_json_api_setup/rspec/matchers/have_resource_count_of.rb +1 -1
- data/lib/infinum_json_api_setup/version.rb +1 -1
- data/lib/infinum_json_api_setup.rb +3 -0
- data/rails.7.1.gemfile +8 -0
- data/rails.7.1.gemfile.lock +361 -0
- data/rails.8.0.gemfile +7 -0
- data/rails.8.0.gemfile.lock +380 -0
- data/spec/dummy/app/controllers/api/v1/base_controller.rb +1 -0
- data/spec/dummy/app/controllers/api/v1/hello_controller.rb +11 -0
- data/spec/dummy/app/controllers/api/v1/locations_controller.rb +7 -13
- data/spec/dummy/app/models/location.rb +1 -1
- data/spec/dummy/config/database.yml +3 -0
- data/spec/dummy/config/environments/production.rb +1 -1
- data/spec/dummy/config/initializers/i18n.rb +3 -0
- data/spec/dummy/config/locales/de.yml +2 -0
- data/spec/dummy/config/locales/json_api.de.yml +22 -0
- data/spec/dummy/config/locales/json_api.en.yml +1 -0
- data/spec/dummy/config/routes.rb +1 -0
- data/spec/infinum_json_api_setup/rspec/helpers/response_helper_spec.rb +234 -0
- data/spec/infinum_json_api_setup/rspec/matchers/have_empty_data_spec.rb +3 -2
- data/spec/rails_helper.rb +2 -6
- data/spec/requests/api/v1/content_negotiation_spec.rb +10 -2
- data/spec/requests/api/v1/error_handling_spec.rb +37 -19
- data/spec/requests/api/v1/locale_negotiation_spec.rb +66 -0
- data/spec/requests/api/v1/responder_spec.rb +1 -1
- data/spec/requests/api/v1/serializer_options_spec.rb +1 -1
- metadata +46 -99
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -268
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
describe InfinumJsonApiSetup::Rspec::Helpers::ResponseHelper do
|
|
2
|
+
include described_class
|
|
3
|
+
|
|
4
|
+
let(:response) { instance_double('Response', body: JSON.dump(payload)) } # rubocop:disable RSpec/RSpec/VerifiedDoubleReference
|
|
5
|
+
|
|
6
|
+
describe '#json_response' do
|
|
7
|
+
let(:payload) do
|
|
8
|
+
{
|
|
9
|
+
data: {
|
|
10
|
+
id: '1',
|
|
11
|
+
type: 'locations',
|
|
12
|
+
attributes: { name: 'Downtown HQ' }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'parses the response body with symbolized keys' do
|
|
18
|
+
expect(json_response).to eq(payload)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#response_item' do
|
|
23
|
+
context 'when the response contains a single resource' do
|
|
24
|
+
let(:payload) do
|
|
25
|
+
{
|
|
26
|
+
data: {
|
|
27
|
+
id: '1',
|
|
28
|
+
type: 'locations',
|
|
29
|
+
attributes: { name: 'Downtown HQ', city: 'Zagreb' }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'returns an OpenStruct built from the resource attributes' do
|
|
35
|
+
expect(response_item).to have_attributes(name: 'Downtown HQ', city: 'Zagreb')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'when the response contains a collection' do
|
|
40
|
+
let(:payload) do
|
|
41
|
+
{
|
|
42
|
+
data: [
|
|
43
|
+
{ id: '1', type: 'locations', attributes: { name: 'Downtown HQ' } }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'raises an informative error' do
|
|
49
|
+
expect { response_item }.to raise_error('json response is not an item')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#response_collection' do
|
|
55
|
+
context 'when the response contains a collection' do
|
|
56
|
+
let(:payload) do
|
|
57
|
+
{
|
|
58
|
+
data: [
|
|
59
|
+
{ id: '1', type: 'locations', attributes: { name: 'Downtown HQ' } },
|
|
60
|
+
{ id: '2', type: 'locations', attributes: { name: 'Suburb Office' } }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'wraps each resource in an OpenStruct including id and type' do
|
|
66
|
+
expect(response_collection).to contain_exactly(
|
|
67
|
+
have_attributes(id: '1', type: 'locations', name: 'Downtown HQ'),
|
|
68
|
+
have_attributes(id: '2', type: 'locations', name: 'Suburb Office')
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context 'when the response contains a single resource' do
|
|
74
|
+
let(:payload) do
|
|
75
|
+
{
|
|
76
|
+
data: {
|
|
77
|
+
id: '1',
|
|
78
|
+
type: 'locations',
|
|
79
|
+
attributes: { name: 'Downtown HQ' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'raises an informative error' do
|
|
85
|
+
expect { response_collection }.to raise_error('json response is not a collection')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#response_relationships' do
|
|
91
|
+
context 'when the response contains a single resource' do
|
|
92
|
+
let(:payload) do
|
|
93
|
+
{
|
|
94
|
+
data: {
|
|
95
|
+
relationships: {
|
|
96
|
+
label: { data: { id: '2', type: 'location_labels' } }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns the relationships hash for the primary resource' do
|
|
103
|
+
expect(response_relationships).to eq(payload[:data][:relationships])
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
context 'when the response contains a collection' do
|
|
108
|
+
let(:payload) do
|
|
109
|
+
{
|
|
110
|
+
data: [
|
|
111
|
+
{ relationships: { label: { data: { id: '2', type: 'location_labels' } } } },
|
|
112
|
+
{ relationships: { label: { data: { id: '3', type: 'location_labels' } } } }
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns the relationships for each resource' do
|
|
118
|
+
expect(response_relationships(response_type: :collection)).to eq(
|
|
119
|
+
payload[:data].pluck(:relationships)
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context 'when an unsupported response type is requested' do
|
|
125
|
+
let(:payload) do
|
|
126
|
+
{
|
|
127
|
+
data: {
|
|
128
|
+
relationships: {
|
|
129
|
+
label: { data: { id: '2', type: 'location_labels' } }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'raises an argument error' do
|
|
136
|
+
expect { response_relationships(response_type: :unknown) }
|
|
137
|
+
.to raise_error(ArgumentError, ':response_type must be one of [:item, :collection]')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '#response_meta' do
|
|
143
|
+
let(:payload) do
|
|
144
|
+
{
|
|
145
|
+
data: {},
|
|
146
|
+
meta: { current_page: 2, total_pages: 5 }
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'returns the meta section of the response' do
|
|
151
|
+
expect(response_meta).to eq(payload[:meta])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe '#response_included' do
|
|
156
|
+
let(:payload) do
|
|
157
|
+
{
|
|
158
|
+
data: {},
|
|
159
|
+
included: [
|
|
160
|
+
{
|
|
161
|
+
id: '2',
|
|
162
|
+
type: 'location_labels',
|
|
163
|
+
attributes: { name: 'HQ Label' }
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'returns included resources as OpenStruct instances' do
|
|
170
|
+
expect(response_included.first).to have_attributes(id: '2', type: 'location_labels', name: 'HQ Label')
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe '#response_included_relationship' do
|
|
175
|
+
context 'when the relationship exists in the included section' do
|
|
176
|
+
let(:payload) do
|
|
177
|
+
{
|
|
178
|
+
data: {
|
|
179
|
+
relationships: {
|
|
180
|
+
label: { data: { id: '2', type: 'location_labels' } }
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
included: [
|
|
184
|
+
{
|
|
185
|
+
id: '2',
|
|
186
|
+
type: 'location_labels',
|
|
187
|
+
attributes: { name: 'HQ Label' }
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'returns the matching included resource' do
|
|
194
|
+
result = response_included_relationship(:label)
|
|
195
|
+
|
|
196
|
+
expect(result).to have_attributes(id: '2', type: 'location_labels', name: 'HQ Label')
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
context 'when the relationship data is nil' do
|
|
201
|
+
let(:payload) do
|
|
202
|
+
{
|
|
203
|
+
data: {
|
|
204
|
+
relationships: {
|
|
205
|
+
label: { data: nil }
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
included: []
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'returns nil' do
|
|
213
|
+
expect(response_included_relationship(:label)).to be_nil
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
context 'when no matching included resource is present' do
|
|
218
|
+
let(:payload) do
|
|
219
|
+
{
|
|
220
|
+
data: {
|
|
221
|
+
relationships: {
|
|
222
|
+
label: { data: { id: '99', type: 'location_labels' } }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
included: []
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'returns nil' do
|
|
230
|
+
expect(response_included_relationship(:label)).to be_nil
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -12,11 +12,12 @@ describe InfinumJsonApiSetup::RSpec::Matchers::HaveEmptyData do
|
|
|
12
12
|
|
|
13
13
|
context "when data isn't empty" do
|
|
14
14
|
it 'fails and describes failure reason' do
|
|
15
|
-
|
|
15
|
+
data = { 'a' => 1 }
|
|
16
|
+
response = response_with_body(JSON.dump(data:))
|
|
16
17
|
|
|
17
18
|
expect do
|
|
18
19
|
expect(response).to have_empty_data
|
|
19
|
-
end.to fail_with("Expected response data({
|
|
20
|
+
end.to fail_with("Expected response data(#{data}) to be empty, but isn't")
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
|
data/spec/rails_helper.rb
CHANGED
|
@@ -27,20 +27,16 @@ require 'pundit'
|
|
|
27
27
|
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
|
28
28
|
# require only the support files necessary.
|
|
29
29
|
#
|
|
30
|
-
Dir[File.expand_path('support/**/*.rb', __dir__)].
|
|
30
|
+
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
|
|
31
31
|
|
|
32
32
|
# Checks for pending migrations and applies them before tests are run.
|
|
33
33
|
# If you are not using ActiveRecord, you can remove these lines.
|
|
34
34
|
begin
|
|
35
35
|
ActiveRecord::Migration.maintain_test_schema!
|
|
36
36
|
rescue ActiveRecord::PendingMigrationError => e
|
|
37
|
-
|
|
38
|
-
exit 1
|
|
37
|
+
abort e.to_s.strip
|
|
39
38
|
end
|
|
40
39
|
RSpec.configure do |config|
|
|
41
|
-
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
|
42
|
-
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
|
43
|
-
|
|
44
40
|
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
|
45
41
|
# examples within a transaction, remove the following line or assign false
|
|
46
42
|
# instead of true.
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
describe 'Content negotiation'
|
|
1
|
+
describe 'Content negotiation' do
|
|
2
2
|
it 'passes through requests demanding JSON:API compliant response' do
|
|
3
3
|
get '/api/v1/locations', headers: { accept: 'application/vnd.api+json' }
|
|
4
4
|
|
|
5
5
|
expect(response).to have_http_status(:ok)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
it 'passes through requests containing JSON:API compliant body' do
|
|
9
|
+
post '/api/v1/locations', params: { location: { latitude: 1, longitude: 1 } }.to_json,
|
|
10
|
+
headers: { accept: 'application/vnd.api+json',
|
|
11
|
+
'content-type': 'application/vnd.api+json; charset=utf-8' }
|
|
12
|
+
|
|
13
|
+
expect(response).to have_http_status(:created)
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
it 'responds with 406 NotAcceptable to requests demanding non JSON:API compliant reponse' do
|
|
9
17
|
get '/api/v1/locations', headers: { accept: 'application/json' }
|
|
10
18
|
|
|
@@ -12,7 +20,7 @@ describe 'Content negotiation', type: :request do
|
|
|
12
20
|
end
|
|
13
21
|
|
|
14
22
|
it 'responds with 415 UnsupportedMediaType to requests containing non JSON:API compliant body' do
|
|
15
|
-
post '/api/v1/locations', params: {
|
|
23
|
+
post '/api/v1/locations', params: { location: { latitude: 1, longitude: 1 } },
|
|
16
24
|
headers: { accept: 'application/vnd.api+json',
|
|
17
25
|
'content-type': 'application/x-www-form-urlencoded' }
|
|
18
26
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
describe 'Error handling'
|
|
1
|
+
describe 'Error handling' do
|
|
2
2
|
context "when request doesn't contain required parameters" do
|
|
3
3
|
it 'responds with 400 BadRequest' do
|
|
4
4
|
post '/api/v1/locations', params: {}, headers: default_headers
|
|
5
5
|
|
|
6
6
|
expect(response).to have_http_status(:bad_request)
|
|
7
7
|
error = json_response['errors'].first
|
|
8
|
-
expect(error
|
|
9
|
-
expect(error['detail']).to match('param is missing or the value is empty: location')
|
|
8
|
+
expect(error).to include('title' => 'Bad Request', 'detail' => match(/param is missing or the value is empty/))
|
|
10
9
|
end
|
|
11
10
|
end
|
|
12
11
|
|
|
@@ -17,7 +16,7 @@ describe 'Error handling', type: :request do
|
|
|
17
16
|
post '/api/v1/locations', params: { location: params }.to_json, headers: default_headers
|
|
18
17
|
|
|
19
18
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
20
|
-
expect(json_response['errors'].
|
|
19
|
+
expect(json_response['errors'].pluck('source')).to contain_exactly(
|
|
21
20
|
{ 'parameter' => 'latitude', 'pointer' => 'data/attributes/latitude' },
|
|
22
21
|
{ 'parameter' => 'longitude', 'pointer' => 'data/attributes/longitude' }
|
|
23
22
|
)
|
|
@@ -29,25 +28,44 @@ describe 'Error handling', type: :request do
|
|
|
29
28
|
get '/api/v1/locations/0', headers: default_headers
|
|
30
29
|
|
|
31
30
|
expect(response).to have_http_status(:not_found)
|
|
32
|
-
expect(json_response['errors'].first
|
|
33
|
-
|
|
31
|
+
expect(json_response['errors'].first).to include('title' => 'Not found', 'detail' => 'Resource not found')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'with another locale' do
|
|
35
|
+
it 'responds with localized error message' do
|
|
36
|
+
get '/api/v1/locations/0', headers: default_headers.merge('Accept-Language' => 'de')
|
|
37
|
+
|
|
38
|
+
expect(response).to have_http_status(:not_found)
|
|
39
|
+
expect(json_response['errors'].first).to include('title' => 'Nicht gefunden',
|
|
40
|
+
'detail' => 'Ressource nicht gefunden')
|
|
41
|
+
end
|
|
34
42
|
end
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
context 'when client is not authorized to perform requested action' do
|
|
46
|
+
let(:loc) { create(:location, :fourth_quadrant) }
|
|
47
|
+
|
|
38
48
|
it 'responds with 403 Forbidden' do
|
|
39
|
-
loc = create(:location, :fourth_quadrant)
|
|
40
49
|
get "/api/v1/locations/#{loc.id}", headers: default_headers
|
|
41
50
|
|
|
42
51
|
expect(response).to have_http_status(:forbidden)
|
|
43
|
-
expect(json_response['errors'].first
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
expect(json_response['errors'].first).to include('title' => 'Forbidden',
|
|
53
|
+
'detail' => 'You are not allowed to perform this action')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context 'with another locale' do
|
|
57
|
+
it 'responds with localized error message' do
|
|
58
|
+
get "/api/v1/locations/#{loc.id}", headers: default_headers.merge('Accept-Language' => 'de')
|
|
59
|
+
|
|
60
|
+
expect(response).to have_http_status(:forbidden)
|
|
61
|
+
expect(json_response['errors'].first).to include('title' => 'Verboten',
|
|
62
|
+
'detail' => 'Sie dürfen diese Aktion nicht ausführen')
|
|
63
|
+
end
|
|
46
64
|
end
|
|
47
65
|
end
|
|
48
66
|
|
|
49
67
|
context 'when request contains unpermitted sort params' do
|
|
50
|
-
let(:bugsnag) { class_double('Bugsnag', notify: nil) }
|
|
68
|
+
let(:bugsnag) { class_double('Bugsnag', notify: nil) } # rubocop:disable RSpec/VerifiedDoubleReference
|
|
51
69
|
|
|
52
70
|
before do
|
|
53
71
|
stub_const('Bugsnag', bugsnag)
|
|
@@ -64,14 +82,14 @@ describe 'Error handling', type: :request do
|
|
|
64
82
|
|
|
65
83
|
expect(response).to have_http_status(:bad_request)
|
|
66
84
|
error = json_response['errors'].first
|
|
67
|
-
expect(error
|
|
68
|
-
|
|
85
|
+
expect(error).to include('title' => 'Bad Request',
|
|
86
|
+
'detail' => 'title is not a permitted sort attribute')
|
|
69
87
|
end
|
|
70
88
|
end
|
|
71
89
|
|
|
72
90
|
context 'when action processing causes PG::Error' do
|
|
73
|
-
let(:location_model) { class_double(
|
|
74
|
-
let(:bugsnag) { class_double('Bugsnag', notify: nil) }
|
|
91
|
+
let(:location_model) { class_double(Location) }
|
|
92
|
+
let(:bugsnag) { class_double('Bugsnag', notify: nil) } # rubocop:disable RSpec/VerifiedDoubleReference
|
|
75
93
|
|
|
76
94
|
before do
|
|
77
95
|
stub_const('Bugsnag', bugsnag)
|
|
@@ -89,14 +107,14 @@ describe 'Error handling', type: :request do
|
|
|
89
107
|
get '/api/v1/locations/0', headers: default_headers
|
|
90
108
|
|
|
91
109
|
expect(response).to have_http_status(:internal_server_error)
|
|
92
|
-
expect(json_response['errors'].first
|
|
93
|
-
|
|
110
|
+
expect(json_response['errors'].first).to include('title' => 'Internal Server Error',
|
|
111
|
+
'detail' => 'Something went wrong')
|
|
94
112
|
end
|
|
95
113
|
end
|
|
96
114
|
|
|
97
115
|
context 'when client requests invalid locale' do
|
|
98
|
-
it 'responds with
|
|
99
|
-
get '/api/v1/locations
|
|
116
|
+
it 'responds with 400 BadRequest' do
|
|
117
|
+
get '/api/v1/locations', headers: default_headers.merge('Accept-Language' => 'fr')
|
|
100
118
|
|
|
101
119
|
expect(response).to have_http_status(:bad_request)
|
|
102
120
|
expect(json_response['errors'].first['title']).to eq('Bad Request')
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
describe 'Locale negotiation' do
|
|
2
|
+
describe 'with localized content' do
|
|
3
|
+
it 'returns English hello message when Accept-Language is en' do
|
|
4
|
+
get '/api/v1/hello', headers: default_headers.merge('Accept-Language': 'en')
|
|
5
|
+
|
|
6
|
+
expect(response).to have_http_status(:ok)
|
|
7
|
+
|
|
8
|
+
expect(json_response['data']['attributes']['message']).to eq('Hello world')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns German hello message when Accept-Language is de' do
|
|
12
|
+
get '/api/v1/hello', headers: default_headers.merge('Accept-Language': 'de')
|
|
13
|
+
|
|
14
|
+
expect(response).to have_http_status(:ok)
|
|
15
|
+
|
|
16
|
+
expect(json_response['data']['attributes']['message']).to eq('Hallo Welt')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns default English message when no Accept-Language header provided' do
|
|
20
|
+
get '/api/v1/hello', headers: default_headers
|
|
21
|
+
|
|
22
|
+
expect(response).to have_http_status(:ok)
|
|
23
|
+
|
|
24
|
+
expect(json_response['data']['attributes']['message']).to eq('Hello world')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'prioritizes first valid locale from complex Accept-Language header' do
|
|
28
|
+
get '/api/v1/hello', headers: default_headers.merge('Accept-Language': 'de-DE,de;q=0.9,en;q=0.8')
|
|
29
|
+
|
|
30
|
+
expect(response).to have_http_status(:ok)
|
|
31
|
+
|
|
32
|
+
expect(json_response['data']['attributes']['message']).to eq('Hallo Welt')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe 'error handling' do
|
|
37
|
+
it 'responds with 400 Bad Request when Accept-Language header is malformed' do
|
|
38
|
+
get '/api/v1/hello', headers: default_headers.merge('Accept-Language': '123-invalid')
|
|
39
|
+
|
|
40
|
+
expect(response).to have_http_status(:bad_request)
|
|
41
|
+
expect(response).to include_error_detail('Invalid locale')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'responds with 400 Bad Request and error message for invalid locale' do
|
|
45
|
+
get '/api/v1/hello', headers: default_headers.merge('Accept-Language': 'fr')
|
|
46
|
+
|
|
47
|
+
expect(response).to have_http_status(:bad_request)
|
|
48
|
+
expect(response).to include_error_detail('Invalid locale')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when fallback to default is enabled' do
|
|
52
|
+
around do |example|
|
|
53
|
+
Api::V1::HelloController.fallback_to_default_locale_if_invalid = true
|
|
54
|
+
example.run
|
|
55
|
+
Api::V1::HelloController.fallback_to_default_locale_if_invalid = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'responds with 200 OK and default locale body' do
|
|
59
|
+
get '/api/v1/hello', headers: default_headers.merge('Accept-Language': 'fr')
|
|
60
|
+
|
|
61
|
+
expect(response).to have_http_status(:ok)
|
|
62
|
+
expect(json_response['data']['attributes']['message']).to eq('Hello world')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|