haveapi 0.29.1 → 0.29.3

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.
@@ -5,33 +5,57 @@
5
5
  en:
6
6
  haveapi:
7
7
  action_state:
8
- cancellation_failed: cancellation failed
9
- not_found: action state not found
10
- timeout_max: timeout has to be maximally %{max}
8
+ cancellation_failed: Action cancellation failed
9
+ not_found: Action state not found
10
+ timeout_max: timeout must be at most %{max}
11
11
  authentication:
12
- failed: authentication failed
13
- invalid_credentials: invalid authentication credentials
14
- multiple_oauth2_tokens: Multiple OAuth2 tokens provided
15
- multiple_tokens: Multiple authentication tokens provided
16
- renew_failed: renew failed
17
- required: Action requires user to authenticate
18
- revoke_failed: revoke failed
19
- token_required: Action requires user to authenticate with a token
12
+ basic:
13
+ description: |-
14
+ Authentication using HTTP Basic. The username and password are passed in an HTTP header.
15
+ This method must not be used from web browsers.
16
+ failed: Authentication failed
17
+ invalid_credentials: Invalid authentication credentials
18
+ multiple_oauth2_tokens: Multiple OAuth2 tokens were provided
19
+ multiple_tokens: Multiple authentication tokens were provided
20
+ oauth2:
21
+ description: |-
22
+ OAuth2 authorization provider.
23
+ HaveAPI clients have limited OAuth2 support, but the API can be used as an authentication source and OAuth2 tokens can be used to access it. HaveAPI partially implements RFC 6749: authorization response type "code" and token grant types "authorization_code" and "refresh_token". Other response and grant types are not supported at this time. The access token can be passed as a bearer token according to RFC 6750, or using a custom HTTP header when the Authorization header is not practical.
24
+ Access and refresh tokens can be revoked according to RFC 7009.
25
+ renew_failed: Token renewal failed
26
+ required: This action requires authentication
27
+ revoke_failed: Token revocation failed
28
+ token:
29
+ actions:
30
+ renew:
31
+ description: Renew the current authentication token
32
+ request:
33
+ description: Request an authentication token
34
+ revoke:
35
+ description: Revoke the current authentication token
36
+ description: The client authenticates with credentials, usually a username
37
+ and password, and receives a token. From that point on, the credentials
38
+ can be forgotten and the token is used instead. Tokens can have different
39
+ lifetimes and can be renewed or revoked. The token is passed either in an
40
+ HTTP header or as a query parameter.
41
+ resource:
42
+ description: Manage authentication tokens
43
+ token_required: This action requires token authentication
20
44
  authorization:
21
45
  insufficient_permissions: Access denied. Insufficient permissions.
22
46
  errors:
23
47
  action_not_found: Action not found
24
- bad_accept_header: Bad Accept header
25
- bad_json_syntax: Bad JSON syntax
26
- json_body_object: JSON body must be an object
27
- server_error: Server error occurred
28
- unsupported_content_type: Unsupported Content-Type
48
+ bad_accept_header: Invalid Accept header
49
+ bad_json_syntax: Invalid JSON syntax
50
+ json_body_object: The JSON request body must be an object
51
+ server_error: A server error occurred
52
+ unsupported_content_type: Unsupported Content-Type header
29
53
  pagination:
30
- limit_max: limit has to be maximally %{max}
54
+ limit_max: limit must be at most %{max}
31
55
  parameters:
32
56
  action_state:
33
57
  can_cancel:
34
- description: When true, execution of this action can be cancelled
58
+ description: When true, this action can be cancelled
35
59
  label: Can cancel
36
60
  created_at:
37
61
  label: Created at
@@ -43,60 +67,59 @@ en:
43
67
  label: Label
44
68
  poll:
45
69
  current:
46
- description: progress to check with if update_in is set
70
+ description: Current progress value to compare when update_in is set
47
71
  status:
48
- description: status to check with if update_in is set
72
+ description: Status value to compare when update_in is set
49
73
  timeout:
50
- description: in seconds
74
+ description: Timeout in seconds
51
75
  label: Timeout
52
76
  total:
53
- description: progress to check with if update_in is set
77
+ description: Total progress value to compare when update_in is set
54
78
  update_in:
55
- description: number of seconds after which the state is returned if the
56
- progress has changed
57
- label: Progress
79
+ description: Return early when status, current, or total changes; the
80
+ value is passed to API-specific polling implementations
81
+ label: Update in
58
82
  status:
59
- description: Determines whether the action is proceeding or failing
83
+ description: Whether the action is proceeding successfully or has failed
60
84
  label: Status
61
85
  total:
62
- description: The action is finished when current equals to total
86
+ description: The action is finished when current reaches total
63
87
  label: Total
64
88
  unit:
65
89
  description: Unit of current and total
66
90
  label: Unit
67
91
  updated_at:
68
- description: When was the progress last updated
92
+ description: When the progress was last updated
69
93
  label: Updated at
70
94
  action_state_id:
71
- description: ID of ActionState object for state querying. When null, the action
72
- is not blocking for the current invocation.
95
+ description: |-
96
+ ID of the ActionState object for state polling.
97
+ When null, the action is not blocking for the current invocation.
73
98
  label: Action state ID
74
99
  active_record:
75
100
  includes:
76
- description: |-
77
- A list of names of associated resources separated by a comma.
78
- Nested associations are declared with '__' between resource names.
79
- For example, 'user,node' will resolve the two associations.
80
- To resolve further associations of node, use e.g. 'user,node__location',
81
- to go even deeper, use e.g. 'user,node__location__environment'.
101
+ description: A comma-separated list of associated resource names. Nested
102
+ associations are declared with '__' between resource names. For example,
103
+ 'user,node' includes these two associations. To include further associations
104
+ of node, use e.g. 'user,node__location'; to go even deeper, use e.g. 'user,node__location__environment'.
82
105
  label: Included associations
83
106
  path_params:
84
- description: An array of parameters needed to resolve URL to this object
107
+ description: An array of parameters needed to build the URL for this object
85
108
  label: URL parameters
86
109
  resolved:
87
- description: True if the association is resolved
110
+ description: True if the association has been loaded
88
111
  label: Resolved
89
112
  authentication:
90
113
  token:
91
114
  interval:
92
- description: How long will requested token be valid, in seconds.
115
+ description: How long the requested token will be valid, in seconds.
93
116
  label: Interval
94
117
  lifetime:
95
118
  description: |-
96
- fixed - the token has a fixed validity period, it cannot be renewed
97
- renewable_manual - the token can be renewed, but it must be done manually via renew action
98
- renewable_auto - the token is renewed automatically to now+interval every time it is used
99
- permanent - the token will be valid forever, unless deleted
119
+ fixed - the token has a fixed validity period and cannot be renewed
120
+ renewable_manual - the token can be renewed, but only manually using the renew action
121
+ renewable_auto - the token is renewed automatically to now + interval whenever it is used
122
+ permanent - the token is valid forever unless deleted
100
123
  label: Lifetime
101
124
  password:
102
125
  label: Password
@@ -106,39 +129,52 @@ en:
106
129
  label: User
107
130
  default:
108
131
  count:
109
- label: Return the count of all items
132
+ label: Return total count
110
133
  total_count:
111
- label: Total count of all items
134
+ label: Total item count
112
135
  metadata:
113
136
  'no':
114
137
  label: Disable metadata
115
138
  paginable:
116
139
  from_id:
117
- description: List objects with greater/lesser ID
140
+ description: List objects with IDs greater or lower than this value
118
141
  label: From ID
119
142
  limit:
120
143
  description: Number of objects to retrieve
121
144
  label: Limit
145
+ resources:
146
+ action_state:
147
+ actions:
148
+ cancel:
149
+ description: Cancel a pending action
150
+ index:
151
+ description: List pending action states
152
+ poll:
153
+ description: Return when the action is completed, its progress changes,
154
+ or a timeout occurs
155
+ show:
156
+ description: Show the state of a pending action
157
+ description: Browse states of blocking actions
122
158
  types:
123
- invalid_boolean: not a valid boolean %{value}
124
- invalid_datetime: not in ISO 8601 format '%{value}'
125
- invalid_float: not a valid float %{value}
126
- invalid_integer: not a valid integer %{value}
127
- invalid_string: not a valid string %{value}
159
+ invalid_boolean: 'not a valid boolean: %{value}'
160
+ invalid_datetime: 'not in ISO 8601 format: ''%{value}'''
161
+ invalid_float: 'not a valid float: %{value}'
162
+ invalid_integer: 'not a valid integer: %{value}'
163
+ invalid_string: 'not a valid string: %{value}'
128
164
  validation:
129
165
  cannot_be_null: cannot be null
130
166
  includes_only_strings: includes must contain only strings
131
167
  includes_string_or_array: includes must be a string or array
132
- input_parameters_not_valid: input parameters not valid
133
- invalid_id: not a valid id %{value}
168
+ input_parameters_not_valid: input parameters are not valid
169
+ invalid_id: 'not a valid ID: %{value}'
134
170
  invalid_input_layout: invalid input layout
135
171
  invalid_path_parameter_encoding: invalid path parameter encoding
136
172
  invalid_string_encoding: invalid string encoding
137
- required_parameter_missing: required parameter missing
173
+ required_parameter_missing: required parameter is missing
138
174
  resource_not_found: resource not found
139
175
  validators:
140
176
  acceptance:
141
- accepted: has to be %{value}
177
+ accepted: must be %{value}
142
178
  confirmation:
143
179
  different: must be different from %{parameter}
144
180
  same: must be the same as %{parameter}
@@ -149,20 +185,20 @@ en:
149
185
  inclusion:
150
186
  included: "%{value} cannot be used"
151
187
  length:
152
- equals: length has to be %{equals}
153
- max: length has to be maximally %{max}
154
- min: length has to be minimally %{min}
155
- range: length has to be in range <%{min}, %{max}>
188
+ equals: length must be %{equals}
189
+ max: length must be at most %{max}
190
+ min: length must be at least %{min}
191
+ range: length must be between %{min} and %{max}
156
192
  numericality:
157
193
  composite: "%{requirements}"
158
- even: even
159
- max: has to be maximally %{max}
160
- min: has to be minimally %{min}
161
- mod: mod %{mod} must equal zero
162
- number: has to be a number
163
- odd: odd
164
- range: has to be in range <%{min}, %{max}>
165
- step: in steps of %{step}
194
+ even: must be even
195
+ max: must be at most %{max}
196
+ min: must be at least %{min}
197
+ mod: remainder after division by %{mod} must be zero
198
+ number: must be a number
199
+ odd: must be odd
200
+ range: must be between %{min} and %{max}
201
+ step: must be in steps of %{step}
166
202
  presence:
167
203
  non_empty: must be present and non-empty
168
204
  present: must be present
@@ -14,7 +14,7 @@ module HaveAPI::Parameters
14
14
  keys:,
15
15
  value:
16
16
  }
17
- end
17
+ end + choice_i18n_catalog_items(context, i18n_path)
18
18
  end
19
19
 
20
20
  private
@@ -27,6 +27,17 @@ module HaveAPI::Parameters
27
27
  localized_metadata(context, i18n_path, :description, @desc)
28
28
  end
29
29
 
30
+ def localized_validators(context, i18n_path, validators)
31
+ include_validator = validators[:include]
32
+ return validators unless include_validator && include_validator.has_key?(:values)
33
+
34
+ values = include_validator.fetch(:values)
35
+ localized_values = localized_choice_values(context, i18n_path, values)
36
+ return validators if localized_values.equal?(values)
37
+
38
+ validators.merge(include: include_validator.merge(values: localized_values))
39
+ end
40
+
30
41
  def localized_metadata(context, i18n_path, kind, fallback)
31
42
  keys = metadata_i18n_keys(context, i18n_path, kind, fallback)
32
43
  default = HaveAPI.localize(fallback)
@@ -61,6 +72,74 @@ module HaveAPI::Parameters
61
72
  [exact_key, *metadata_i18n_default_keys(context, i18n_path, kind, scope)].uniq
62
73
  end
63
74
 
75
+ def localized_choice_values(context, i18n_path, values)
76
+ case values
77
+ when ::Hash
78
+ values.to_h do |value, label|
79
+ [
80
+ value,
81
+ localized_metadata(context, i18n_path, choice_i18n_kind(value), label)
82
+ ]
83
+ end
84
+ when ::Array
85
+ localized = values.to_h do |value|
86
+ label = localized_metadata(
87
+ context,
88
+ i18n_path,
89
+ choice_i18n_kind(value),
90
+ value.to_s
91
+ )
92
+ [value, label]
93
+ end
94
+
95
+ localized.any? { |value, label| label.to_s != value.to_s } ? localized : values
96
+ else
97
+ values
98
+ end
99
+ end
100
+
101
+ def choice_i18n_catalog_items(context, i18n_path)
102
+ values = choice_i18n_values
103
+ return [] unless values
104
+
105
+ choice_i18n_fallbacks(values).filter_map do |value, fallback|
106
+ keys = metadata_i18n_keys(context, i18n_path, choice_i18n_kind(value), fallback)
107
+ label = HaveAPI.localize(fallback).to_s.strip
108
+
109
+ next if keys.empty? || label.empty?
110
+
111
+ {
112
+ param: HaveAPI::Params.i18n_segment(@name),
113
+ kind: metadata_i18n_key_suffix(choice_i18n_kind(value)),
114
+ keys:,
115
+ value: label
116
+ }
117
+ end
118
+ end
119
+
120
+ def choice_i18n_values
121
+ metadata_i18n_choice_values
122
+ end
123
+
124
+ def metadata_i18n_choice_values
125
+ nil
126
+ end
127
+
128
+ def choice_i18n_fallbacks(values)
129
+ case values
130
+ when ::Hash
131
+ values
132
+ when ::Array
133
+ values.to_h { |value| [value, value.to_s] }
134
+ else
135
+ {}
136
+ end
137
+ end
138
+
139
+ def choice_i18n_kind(value)
140
+ "choices.#{HaveAPI::Params.i18n_segment(value)}.label"
141
+ end
142
+
64
143
  def metadata_i18n_fallback(kind)
65
144
  case kind
66
145
  when :label
@@ -78,8 +157,6 @@ module HaveAPI::Parameters
78
157
  @label_key
79
158
  when :description
80
159
  @desc_key
81
- else
82
- raise ArgumentError, "unsupported parameter metadata kind #{kind.inspect}"
83
160
  end
84
161
  end
85
162
 
@@ -62,7 +62,7 @@ module HaveAPI::Parameters
62
62
  label: localized_label(context, i18n_path),
63
63
  description: localized_description(context, i18n_path),
64
64
  type: @type ? @type.to_s : String.to_s,
65
- validators: @validators ? @validators.describe : {},
65
+ validators: @validators ? localized_validators(context, i18n_path, @validators.describe) : {},
66
66
  default: @default,
67
67
  protected: @protected || false
68
68
  }
@@ -167,6 +167,10 @@ module HaveAPI::Parameters
167
167
  @type == Custom
168
168
  end
169
169
 
170
+ def metadata_i18n_choice_values
171
+ @validators&.describe&.dig(:include, :values)
172
+ end
173
+
170
174
  def normalize_custom_keys(value)
171
175
  case value
172
176
  when ::Hash
@@ -2,7 +2,7 @@ require 'haveapi/resource'
2
2
 
3
3
  module HaveAPI::Resources
4
4
  class ActionState < HaveAPI::Resource
5
- desc 'Browse states of blocking actions'
5
+ desc HaveAPI.message('haveapi.resources.action_state.description')
6
6
  auth false
7
7
  version :all
8
8
 
@@ -54,7 +54,7 @@ module HaveAPI::Resources
54
54
  class Index < HaveAPI::Actions::Default::Index
55
55
  include Mixin
56
56
 
57
- desc 'List states of pending actions'
57
+ desc HaveAPI.message('haveapi.resources.action_state.actions.index.description')
58
58
 
59
59
  input(:hash) do
60
60
  string :order, choices: %w[newest oldest], default: 'newest', fill: true
@@ -85,7 +85,7 @@ module HaveAPI::Resources
85
85
 
86
86
  MAX_TIMEOUT = 30
87
87
 
88
- desc 'Returns when the action is completed or timeout occurs'
88
+ desc HaveAPI.message('haveapi.resources.action_state.actions.poll.description')
89
89
  http_method :get
90
90
  route '{%{resource}_id}/poll'
91
91
 
@@ -157,7 +157,7 @@ module HaveAPI::Resources
157
157
  class Show < HaveAPI::Actions::Default::Show
158
158
  include Mixin
159
159
 
160
- desc 'Show state of a pending action'
160
+ desc HaveAPI.message('haveapi.resources.action_state.actions.show.description')
161
161
 
162
162
  output(:hash) do
163
163
  use :all
@@ -178,6 +178,8 @@ module HaveAPI::Resources
178
178
  end
179
179
 
180
180
  class Cancel < HaveAPI::Action
181
+ desc HaveAPI.message('haveapi.resources.action_state.actions.cancel.description')
182
+
181
183
  http_method :post
182
184
  route '{%{resource}_id}/cancel'
183
185
  blocking true
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
2
  PROTOCOL_VERSION = '2.0'.freeze
3
- VERSION = '0.29.1'.freeze
3
+ VERSION = '0.29.3'.freeze
4
4
  end
@@ -638,7 +638,7 @@ describe HaveAPI::Action do
638
638
 
639
639
  expect(last_response.status).to eq(200)
640
640
  expect(api_response).not_to be_ok
641
- expect(api_response.errors[:confirmed]).to include('required parameter missing')
641
+ expect(api_response.errors[:confirmed]).to include('required parameter is missing')
642
642
 
643
643
  call_api([:Test], :batch, { tests: [{ label: 'one' }], _meta: { confirmed: 'maybe' } })
644
644
 
@@ -33,7 +33,7 @@ describe HaveAPI::Action do
33
33
 
34
34
  expect(last_response.status).to eq(200)
35
35
  expect(api_response).not_to be_ok
36
- expect(api_response.errors[:name]).to include('required parameter missing')
36
+ expect(api_response.errors[:name]).to include('required parameter is missing')
37
37
  end
38
38
  end
39
39
 
@@ -69,7 +69,7 @@ describe HaveAPI::Action do
69
69
 
70
70
  expect(last_response.status).to eq(400)
71
71
  expect(api_response).not_to be_ok
72
- expect(api_response.errors[:name]).to include('required parameter missing')
72
+ expect(api_response.errors[:name]).to include('required parameter is missing')
73
73
  end
74
74
  end
75
75
  end
@@ -184,7 +184,7 @@ describe HaveAPI::Resources::ActionState do
184
184
  get_action '/v1/action_states/999'
185
185
 
186
186
  expect(api_response).not_to be_ok
187
- expect(api_response.message).to eq('action state not found')
187
+ expect(api_response.message).to eq('Action state not found')
188
188
  end
189
189
 
190
190
  it 'poll returns immediately if finished' do
@@ -227,7 +227,7 @@ describe HaveAPI::Resources::ActionState do
227
227
 
228
228
  expect(last_response.status).to eq(200)
229
229
  expect(api_response).not_to be_ok
230
- expect(api_response.errors[:timeout].first).to include('range <0, 30>')
230
+ expect(api_response.errors[:timeout].first).to include('between 0 and 30')
231
231
  end
232
232
 
233
233
  it 'poll returns immediately when update_in check mismatches' do
@@ -274,7 +274,7 @@ describe HaveAPI::Resources::ActionState do
274
274
  get_action '/v1/action_states/999/poll', action_state: { timeout: 0 }
275
275
 
276
276
  expect(api_response).not_to be_ok
277
- expect(api_response.message).to eq('action state not found')
277
+ expect(api_response.message).to eq('Action state not found')
278
278
  end
279
279
 
280
280
  it 'cancel returns ok for true' do
@@ -303,7 +303,7 @@ describe HaveAPI::Resources::ActionState do
303
303
  call_api(:post, '/v1/action_states/1/cancel', {})
304
304
 
305
305
  expect(api_response).not_to be_ok
306
- expect(api_response.message).to eq('cancellation failed')
306
+ expect(api_response.message).to eq('Action cancellation failed')
307
307
  end
308
308
 
309
309
  it 'cancel returns error for NotImplementedError' do
@@ -73,7 +73,7 @@ describe HaveAPI::Authentication::Basic::Provider do
73
73
  expect(last_response.headers['www-authenticate']).to include('Basic realm=')
74
74
 
75
75
  expect(api_response).to be_failed
76
- expect(api_response.message).to include('authenticate')
76
+ expect(api_response.message).to eq('This action requires authentication')
77
77
  expect(seen_users.last).to be_nil
78
78
  end
79
79
 
@@ -123,7 +123,7 @@ describe HaveAPI::Authentication::Basic::Provider do
123
123
  expect(last_response.status).to eq(401)
124
124
  expect(last_response.headers['www-authenticate']).to include('Basic realm=')
125
125
  expect(api_response).to be_failed
126
- expect(api_response.message).to include('authenticate')
126
+ expect(api_response.message).to eq('This action requires authentication')
127
127
  end
128
128
 
129
129
  it 'returns an authentication error envelope from _logout' do
@@ -132,6 +132,6 @@ describe HaveAPI::Authentication::Basic::Provider do
132
132
  expect(last_response.status).to eq(401)
133
133
  expect(last_response.headers['www-authenticate']).to include('Basic realm=')
134
134
  expect(api_response).to be_failed
135
- expect(api_response.message).to include('authenticate')
135
+ expect(api_response.message).to eq('This action requires authentication')
136
136
  end
137
137
  end
@@ -225,7 +225,7 @@ describe HaveAPI::Authentication::OAuth2 do
225
225
 
226
226
  expect(last_response.status).to eq(400)
227
227
  expect(api_response).to be_failed
228
- expect(api_response.message).to eq('Bylo poskytnuto více OAuth2 tokenů')
228
+ expect(api_response.message).to eq('Bylo zadáno více OAuth2 tokenů')
229
229
  end
230
230
 
231
231
  it 'ignores structured access_token query values before backend lookup' do
@@ -248,6 +248,7 @@ describe HaveAPI::Authentication::OAuth2 do
248
248
  end
249
249
 
250
250
  it 'exposes oauth2 provider in version description' do
251
+ header 'Accept-Language', 'cs'
251
252
  call_api(:options, '/v1/')
252
253
 
253
254
  expect(last_response.status).to eq(200)
@@ -257,6 +258,7 @@ describe HaveAPI::Authentication::OAuth2 do
257
258
  expect(auth).to have_key(:oauth2)
258
259
 
259
260
  desc = auth[:oauth2]
261
+ expect(desc[:description]).to start_with('OAuth2 autorizační provider')
260
262
  expect(desc).to have_key(:http_header)
261
263
  expect(desc[:http_header]).to eq('X-HaveAPI-OAuth2-Token')
262
264
 
@@ -290,6 +290,7 @@ describe HaveAPI::Authentication::Token do
290
290
  end
291
291
 
292
292
  it 'exposes token provider in version description' do
293
+ header 'Accept-Language', 'cs'
293
294
  call_api(:options, '/v1/')
294
295
 
295
296
  expect(last_response.status).to eq(200)
@@ -297,11 +298,18 @@ describe HaveAPI::Authentication::Token do
297
298
 
298
299
  auth = api_response[:authentication]
299
300
  expect(auth).to have_key(:token)
301
+ expect(auth[:token][:description]).to start_with('Klient se autentizuje')
300
302
  expect(auth[:token][:http_header]).to eq(AuthSpecToken::Config.http_header)
301
303
  expect(auth[:token][:query_parameter]).to eq(AuthSpecToken::Config.query_parameter.to_s)
302
304
 
303
305
  expect(auth[:token]).to have_key(:resources)
304
306
  expect(auth[:token][:resources]).to have_key(:token)
307
+
308
+ resource = auth[:token][:resources][:token]
309
+ expect(resource[:description]).to eq('Spravovat autentizační tokeny')
310
+ expect(resource[:actions][:request][:description]).to eq('Vyžádat autentizační token')
311
+ expect(resource[:actions][:revoke][:description]).to eq('Zneplatnit aktuální autentizační token')
312
+ expect(resource[:actions][:renew][:description]).to eq('Obnovit aktuální autentizační token')
305
313
  end
306
314
  end
307
315
 
@@ -113,7 +113,7 @@ describe HaveAPI::Extensions::ExceptionMailer do
113
113
 
114
114
  expect(last_response.status).to eq(500)
115
115
  expect(api_response).not_to be_ok
116
- expect(api_response.message).to eq('Server error occurred')
116
+ expect(api_response.message).to eq('A server error occurred')
117
117
 
118
118
  expect(calls.size).to eq(1)
119
119
  context, exception, body = calls.first
@@ -190,6 +190,6 @@ describe HaveAPI::Extensions::ExceptionMailer do
190
190
 
191
191
  expect(last_response.status).to eq(500)
192
192
  expect(api_response).not_to be_ok
193
- expect(api_response.message).to eq('Server error occurred')
193
+ expect(api_response.message).to eq('A server error occurred')
194
194
  end
195
195
  end