erratum 0.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.txt +19 -0
  4. data/README.md +2 -0
  5. data/Rakefile +2 -0
  6. data/lib/erratum/configuration.rb +44 -0
  7. data/lib/erratum/error.rb +110 -0
  8. data/lib/erratum/errors/authentication_error.rb +9 -0
  9. data/lib/erratum/errors/authentication_errors/duplicate_authentication_error.rb +37 -0
  10. data/lib/erratum/errors/authentication_errors/invalid_token_error.rb +30 -0
  11. data/lib/erratum/errors/authentication_errors/invalid_username_or_password_error.rb +30 -0
  12. data/lib/erratum/errors/authorization_error.rb +9 -0
  13. data/lib/erratum/errors/authorization_errors/forbidden_error.rb +48 -0
  14. data/lib/erratum/errors/crud_error.rb +24 -0
  15. data/lib/erratum/errors/crud_errors/association_error.rb +54 -0
  16. data/lib/erratum/errors/crud_errors/resource_not_found_error.rb +52 -0
  17. data/lib/erratum/errors/crud_errors/resource_persistence_error.rb +50 -0
  18. data/lib/erratum/errors/request_errors/parameter_missing_error.rb +43 -0
  19. data/lib/erratum/errors/request_errors/unpermitted_parameters_error.rb +49 -0
  20. data/lib/erratum/rescuable_resource.rb +29 -0
  21. data/lib/erratum/resource_naming.rb +31 -0
  22. data/lib/erratum/utilities/string.rb +18 -0
  23. data/lib/erratum/verifiable_resource.rb +23 -0
  24. data/lib/erratum/version.rb +4 -0
  25. data/lib/erratum.rb +48 -0
  26. data/spec/lib/erratum/configuration_spec.rb +27 -0
  27. data/spec/lib/erratum/error_spec.rb +189 -0
  28. data/spec/lib/erratum/errors/authentication_errors/duplicate_authentication_error_spec.rb +43 -0
  29. data/spec/lib/erratum/errors/authentication_errors/invalid_token_error_spec.rb +33 -0
  30. data/spec/lib/erratum/errors/authentication_errors/invalid_username_or_password_error_spec.rb +35 -0
  31. data/spec/lib/erratum/errors/authorization_errors/forbidden_error_spec.rb +52 -0
  32. data/spec/lib/erratum/errors/crud_errors/association_error_spec.rb +69 -0
  33. data/spec/lib/erratum/errors/crud_errors/resource_not_found_error_spec.rb +89 -0
  34. data/spec/lib/erratum/errors/crud_errors/resource_persistence_error_spec.rb +84 -0
  35. data/spec/lib/erratum/errors/request_errors/parameter_missing_error_spec.rb +58 -0
  36. data/spec/lib/erratum/errors/request_errors/unpermitted_parameters_error_spec.rb +67 -0
  37. data/spec/lib/erratum/rescuable_resource_spec.rb +8 -0
  38. data/spec/lib/human_error_spec.rb +20 -0
  39. data.tar.gz.sig +0 -0
  40. metadata +171 -0
  41. metadata.gz.sig +0 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ class Erratum
3
+ module ResourceNaming
4
+ CONTROLLER_RESOURCE_NAME_PATTERN = /\A((.*?::)?.*?)(\w+)Controller\z/
5
+
6
+ module ClassMethods
7
+ def plural_resource_name
8
+ @plural_resource_name ||= name[CONTROLLER_RESOURCE_NAME_PATTERN, 3].
9
+ underscore.
10
+ pluralize.
11
+ downcase
12
+ end
13
+
14
+ def singular_resource_name
15
+ @singular_resource_name ||= name[CONTROLLER_RESOURCE_NAME_PATTERN, 3].
16
+ underscore.
17
+ singularize.
18
+ downcase
19
+ end
20
+
21
+ def resource_class_name
22
+ @resource_class_name ||= singular_resource_name.
23
+ camelize
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ class Erratum
3
+ module Utilities
4
+ class String
5
+ def self.underscore(other)
6
+ word = other.to_s.gsub('::', '/')
7
+ word.gsub!(/(?:([A-Za-z\d])|^)(?=\b|[^a-z])/) do
8
+ "#{Regexp.last_match(1)}#{Regexp.last_match(1) && ''}"
9
+ end
10
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
11
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
12
+ word.tr!('-', '_')
13
+ word.downcase!
14
+ word
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/resource_naming'
3
+
4
+ class Erratum
5
+ module ResourceNaming
6
+ def self.included(base)
7
+ base.include ResourceNaming
8
+
9
+ base.before_action except: %i{index create} do
10
+ model = public_send(self.class.singular_resource_name)
11
+
12
+ resource_not_found_error = Erratum.build(
13
+ 'ResourceNotFoundError',
14
+ resource_name: self.class.singular_resource_name,
15
+ resource_id: [params[:id]],
16
+ action: action_name,
17
+ )
18
+
19
+ fail resource_not_found_error unless model.persisted?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class Erratum
3
+ VERSION = '0.0.1'
4
+ end
data/lib/erratum.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum/configuration'
3
+ require 'erratum/error'
4
+ require 'erratum/errors/authorization_error'
5
+ require 'erratum/errors/authorization_errors/forbidden_error'
6
+ require 'erratum/errors/authentication_error'
7
+ require 'erratum/errors/authentication_errors/duplicate_authentication_error'
8
+ require 'erratum/errors/authentication_errors/invalid_token_error'
9
+ require 'erratum/errors/authentication_errors/invalid_username_or_password_error'
10
+ require 'erratum/errors/crud_error'
11
+ require 'erratum/errors/crud_errors/association_error'
12
+ require 'erratum/errors/crud_errors/resource_not_found_error'
13
+ require 'erratum/errors/crud_errors/resource_persistence_error'
14
+ require 'erratum/errors/request_errors/parameter_missing_error'
15
+ require 'erratum/errors/request_errors/unpermitted_parameters_error'
16
+ require 'erratum/rescuable_resource'
17
+ require 'erratum/verifiable_resource'
18
+ require 'erratum/version'
19
+
20
+ class Erratum
21
+ def self.fetch(error_type)
22
+ Object.const_get("Erratum::Errors::#{error_type}")
23
+ end
24
+
25
+ def self.build(error_type, overrides = {})
26
+ fetch(error_type).new(overrides)
27
+ end
28
+
29
+ def self.convert(original_error, overrides = {})
30
+ case original_error.class.name
31
+ when 'ActiveRecord::InvalidForeignKey'
32
+ fetch('AssociationError').convert(original_error, overrides)
33
+ when 'ActiveRecord::RecordNotFound'
34
+ fetch('ResourceNotFoundError').convert(original_error, overrides)
35
+ when 'ActiveRecord::RecordInvalid',
36
+ 'ActiveRecord::RecordNotSaved'
37
+ fetch('ResourcePersistenceError').convert(original_error, overrides)
38
+ when 'ActionController::ParameterMissing',
39
+ fetch('ParameterMissingError').convert(original_error, overrides)
40
+ when 'ActionController::UnpermittedParameters'
41
+ fetch('UnpermittedParametersError').convert(original_error, overrides)
42
+ end
43
+ end
44
+
45
+ def self.raise(error_type, **args)
46
+ Kernel.fail build(error_type, **args)
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/configuration'
4
+
5
+ class Erratum
6
+ RSpec.describe Configuration, singletons: true do
7
+ it 'can set the url mappings' do
8
+ configuration = Configuration.instance
9
+ configuration.url_mappings = 'foobarbaz'
10
+
11
+ expect(configuration.url_mappings).to eql 'foobarbaz'
12
+ end
13
+
14
+ it 'can convert itself into a hash' do
15
+ configuration = Configuration.instance
16
+ configuration.url_mappings = {
17
+ 'external_documentation_urls' => 'blah',
18
+ 'developer_documentation_urls' => 'asdf',
19
+ }
20
+
21
+ expect(configuration.to_h).to eql(
22
+ external_documentation_urls: 'blah',
23
+ developer_documentation_urls: 'asdf',
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/error'
4
+
5
+ class CustomError < RuntimeError
6
+ include Erratum::Error
7
+ end
8
+
9
+ class Erratum
10
+ RSpec.describe Error do
11
+ it 'can wrap an error with another error' do
12
+ original_error = RuntimeError.new('My Runtime Error Message')
13
+ wrapped_error = CustomError.wrap(original_error)
14
+
15
+ expect(wrapped_error.class.name).to eql 'CustomError'
16
+ end
17
+
18
+ it 'can wrap the message of an error by identifying the original error type' do
19
+ original_error = RuntimeError.new('My Runtime Error Message')
20
+ wrapped_error = CustomError.wrap(original_error)
21
+
22
+ expect(wrapped_error.message).to eql 'RuntimeError: My Runtime Error Message'
23
+ end
24
+
25
+ it 'can transfer the backtrace of the original error to the wrapped error' do
26
+ original_error = RuntimeError.new('My Runtime Error Message')
27
+ original_error.set_backtrace %w{foo bar baz}
28
+ wrapped_error = CustomError.wrap(original_error)
29
+
30
+ expect(wrapped_error.backtrace).to eql %w{foo bar baz}
31
+ end
32
+
33
+ it 'can have its message explicitly set when it is generated' do
34
+ error = CustomError.new(message: 'My Message')
35
+
36
+ expect(error.message).to eql 'My Message'
37
+ end
38
+
39
+ it 'can have its message explicitly set when it is generated' do
40
+ error = CustomError.new
41
+ allow(error).to receive(:detail).
42
+ and_return('My Developer Message')
43
+
44
+ expect(error.message).to eql 'My Developer Message'
45
+ end
46
+
47
+ it 'can generate error data' do
48
+ custom_error = CustomError.new(
49
+ id: 'identifier',
50
+ http_status: 'flibbity',
51
+ code: 'jibbit',
52
+ title: 'roll dem bones and stones',
53
+ detail: 'I cannot receive any satisfaction',
54
+ source: 'But perhaps if I attempt it one more time, I can',
55
+ developer_documentation_url: 'asimof/jibbit?version=janky',
56
+ external_documentation_url: 'jinkies/87654321',
57
+ )
58
+
59
+ expect(custom_error.as_json).to eql(
60
+ errors: [
61
+ {
62
+ id: 'identifier',
63
+ links: {
64
+ about: 'jinkies/87654321',
65
+ documentation: 'asimof/jibbit?version=janky',
66
+ },
67
+ status: 'flibbity',
68
+ code: 'jibbit',
69
+ title: 'roll dem bones and stones',
70
+ detail: 'I cannot receive any satisfaction',
71
+ source: 'But perhaps if I attempt it one more time, I can',
72
+ },
73
+ ],
74
+ )
75
+ end
76
+
77
+ it 'can extract configuration from the global config if it is not passed in',
78
+ singletons: Erratum::Configuration do
79
+
80
+ Erratum.configure do |config|
81
+ config.url_mappings = {
82
+ 'external_documentation_urls' => {
83
+ 'jibbit' => 'http://example.com/edu',
84
+ },
85
+ 'developer_documentation_urls' => {
86
+ 'jibbit' => 'http://example.com/ddu',
87
+ },
88
+ }
89
+ end
90
+
91
+ custom_error = CustomError.new(
92
+ id: 'identifier',
93
+ http_status: 'flibbity',
94
+ code: 'jibbit',
95
+ title: 'roll dem bones and stones',
96
+ detail: 'I cannot receive any satisfaction',
97
+ source: 'But perhaps if I attempt it one more time, I can',
98
+ )
99
+
100
+ expect(custom_error.as_json).to eql(
101
+ errors: [
102
+ {
103
+ id: 'identifier',
104
+ links: {
105
+ about: 'http://example.com/edu',
106
+ documentation: 'http://example.com/ddu',
107
+ },
108
+ status: 'flibbity',
109
+ code: 'jibbit',
110
+ title: 'roll dem bones and stones',
111
+ detail: 'I cannot receive any satisfaction',
112
+ source: 'But perhaps if I attempt it one more time, I can',
113
+ },
114
+ ],
115
+ )
116
+ end
117
+
118
+ it 'can override the global config if it is set, but an explicit value is passed in',
119
+ singletons: Erratum::Configuration do
120
+
121
+ Erratum.configure do |config|
122
+ config.url_mappings = {
123
+ 'external_documentation_urls' => {
124
+ 'jibbit' => 'http://example.com/edu',
125
+ },
126
+ 'developer_documentation_urls' => {
127
+ 'jibbit' => 'http://example.com/ddu',
128
+ },
129
+ }
130
+ end
131
+
132
+ custom_error = CustomError.new(
133
+ id: 'identifier',
134
+ http_status: 'flibbity',
135
+ code: 'jibbit',
136
+ title: 'roll dem bones and stones',
137
+ detail: 'I cannot receive any satisfaction',
138
+ source: 'But perhaps if I attempt it one more time, I can',
139
+ developer_documentation_url: 'hasimof/jibbit?version=hanky',
140
+ external_documentation_url: 'hinkies/87654321',
141
+ )
142
+
143
+ expect(custom_error.as_json).to eql(
144
+ errors: [
145
+ {
146
+ id: 'identifier',
147
+ links: {
148
+ about: 'hinkies/87654321',
149
+ documentation: 'hasimof/jibbit?version=hanky',
150
+ },
151
+ status: 'flibbity',
152
+ code: 'jibbit',
153
+ title: 'roll dem bones and stones',
154
+ detail: 'I cannot receive any satisfaction',
155
+ source: 'But perhaps if I attempt it one more time, I can',
156
+ },
157
+ ],
158
+ )
159
+ end
160
+
161
+ it 'can handle if it finds no URL mappings' do
162
+ custom_error = CustomError.new(
163
+ id: 'identifier',
164
+ http_status: 'flibbity',
165
+ code: 'jibbit',
166
+ title: 'roll dem bones and stones',
167
+ detail: 'I cannot receive any satisfaction',
168
+ source: 'But perhaps if I attempt it one more time, I can',
169
+ )
170
+
171
+ expect(custom_error.as_json).to eql(
172
+ errors: [
173
+ {
174
+ id: 'identifier',
175
+ links: {
176
+ about: nil,
177
+ documentation: nil,
178
+ },
179
+ status: 'flibbity',
180
+ code: 'jibbit',
181
+ title: 'roll dem bones and stones',
182
+ detail: 'I cannot receive any satisfaction',
183
+ source: 'But perhaps if I attempt it one more time, I can',
184
+ },
185
+ ],
186
+ )
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/errors/authentication_errors/duplicate_authentication_error'
4
+
5
+ class Erratum
6
+ module Errors
7
+ RSpec.describe DuplicateAuthenticationError do
8
+ let(:error) do
9
+ DuplicateAuthenticationError.new(provider: 'flibbity',
10
+ provider_user_id: '12345',
11
+ user_id: '54321')
12
+ end
13
+
14
+ it 'has a status of 409' do
15
+ expect(error.http_status).to eql 409
16
+ end
17
+
18
+ it 'has a code' do
19
+ expect(error.code).to eql 'errors.duplicate_authentication_error'
20
+ end
21
+
22
+ it 'has a title' do
23
+ expect(error.title).to eql 'Duplicate Authentication'
24
+ end
25
+
26
+ it 'can output the detail' do
27
+ expect(error.detail).to eql 'The authentication you attempted to ' \
28
+ 'register has already been registered by ' \
29
+ 'another user. We do not currently support ' \
30
+ 'allowing multiple users to be connected to ' \
31
+ 'the same authentication.'
32
+ end
33
+
34
+ it 'can output the source' do
35
+ expect(error.source).to eql(
36
+ 'provider' => 'flibbity',
37
+ 'provider_user_id' => '12345',
38
+ 'user_id' => '54321',
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/errors/authentication_errors/invalid_token_error'
4
+
5
+ class Erratum
6
+ module Errors
7
+ RSpec.describe InvalidTokenError do
8
+ let(:error) { InvalidTokenError.new }
9
+
10
+ it 'has a status of 401' do
11
+ expect(error.http_status).to eql 401
12
+ end
13
+
14
+ it 'has a code' do
15
+ expect(error.code).to eql 'errors.invalid_token_error'
16
+ end
17
+
18
+ it 'has a title' do
19
+ expect(error.title).to eql 'Invalid Token'
20
+ end
21
+
22
+ it 'can output the detail' do
23
+ expect(error.detail).to eql 'The token you attempted to use for this ' \
24
+ 'request is invalid for this resource. ' \
25
+ 'Please double-check and try again.'
26
+ end
27
+
28
+ it 'can output the source' do
29
+ expect(error.source).to eql(token: '[FILTERED]')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/errors/authentication_errors/invalid_username_or_password_error'
4
+
5
+ class Erratum
6
+ module Errors
7
+ RSpec.describe InvalidUsernameOrPasswordError do
8
+ let(:error) { InvalidUsernameOrPasswordError.new }
9
+
10
+ it 'has a status of 401' do
11
+ expect(error.http_status).to eql 401
12
+ end
13
+
14
+ it 'has a code' do
15
+ expect(error.code).to eql 'errors.invalid_username_or_password_error'
16
+ end
17
+
18
+ it 'has a title' do
19
+ expect(error.title).to eql 'Invalid Username/Password'
20
+ end
21
+
22
+ it 'can output the detail' do
23
+ expect(error.detail).to eql 'Either the username or password passed in ' \
24
+ 'or this request is invalid. Please ' \
25
+ 'double-check and try again.'
26
+ end
27
+
28
+ it 'can output the source' do
29
+ error = InvalidUsernameOrPasswordError.new username: 'neo'
30
+
31
+ expect(error.source).to eql(username: 'neo', password: '[FILTERED]')
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/errors/authorization_errors/forbidden_error'
4
+
5
+ class Erratum
6
+ module Errors
7
+ RSpec.describe ForbiddenError do
8
+ let(:error) { ForbiddenError.new }
9
+
10
+ it 'has a status of 403' do
11
+ expect(error.http_status).to eql 403
12
+ end
13
+
14
+ it 'has a code' do
15
+ expect(error.code).to eql 'errors.forbidden_error'
16
+ end
17
+
18
+ it 'has a title' do
19
+ expect(error.title).to eql 'Forbidden'
20
+ end
21
+
22
+ it 'can output the detail for actions affecting a specific resource' do
23
+ error = ForbiddenError.new(resource_name: 'resource',
24
+ resource_id: '123',
25
+ action: 'show')
26
+
27
+ expect(error.detail).to eql \
28
+ 'You do not have access to show the resource with ID 123. Providing a different ' \
29
+ 'set of credentials may potentially allow you access to this resource.'
30
+ end
31
+
32
+ it 'can output the detail for actions affecting multiple resources' do
33
+ error = ForbiddenError.new(resource_name: 'resource',
34
+ action: 'create')
35
+
36
+ expect(error.detail).to eql \
37
+ 'You do not have access to create a resource. Providing a different ' \
38
+ 'set of credentials may potentially allow you access to this resource.'
39
+ end
40
+
41
+ it 'can output the source' do
42
+ error = ForbiddenError.new(resource_name: 'resource',
43
+ resource_id: '123',
44
+ action: 'show')
45
+
46
+ expect(error.source).to eql('resource_name' => 'resource',
47
+ 'resource_id' => '123',
48
+ 'action' => 'show')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/errors/crud_errors/association_error'
4
+ require 'active_record/errors'
5
+
6
+ class Erratum
7
+ module Errors
8
+ RSpec.describe AssociationError do
9
+ let(:foreign_key_error) do
10
+ ActiveRecord::InvalidForeignKey.new('DETAIL: Key (resource_id)=(123)')
11
+ end
12
+
13
+ it 'has a status of 422' do
14
+ error = AssociationError.new
15
+
16
+ expect(error.http_status).to eql 422
17
+ end
18
+
19
+ it 'has a code' do
20
+ error = AssociationError.new
21
+
22
+ expect(error.code).to eql 'errors.association_error'
23
+ end
24
+
25
+ it 'has a title' do
26
+ error = AssociationError.new
27
+
28
+ expect(error.title).to eql 'Association Error'
29
+ end
30
+
31
+ it 'includes the resource name and action in the detail' do
32
+ error = AssociationError.new association_name: 'dragon',
33
+ resource_name: 'Daenerys'
34
+
35
+ expect(error.detail).to eql 'The dragon that you ' \
36
+ 'attempted to associate with the Daenerys was ' \
37
+ 'not valid.'
38
+ end
39
+
40
+ it 'includes the resource name and action in the source' do
41
+ error = AssociationError.new association_name: 'dragon',
42
+ resource_name: 'Daenerys',
43
+ attributes: 'winter is coming',
44
+ association_id: '123'
45
+
46
+ expect(error.source).to eql(
47
+ 'Daenerys' => 'winter is coming',
48
+ 'dragon id' => '123',
49
+ )
50
+ end
51
+
52
+ it 'can convert an "ActiveRecord::InvalidForeignKey"' do
53
+ error = AssociationError.convert(foreign_key_error)
54
+
55
+ expect(error.resource_name).to eql nil
56
+ expect(error.association_name).to eql 'resource'
57
+ expect(error.association_id).to eql '123'
58
+ end
59
+
60
+ it 'can convert an "ActiveRecord::InvalidForeignKey" while overriding attributes' do
61
+ error = AssociationError.convert(foreign_key_error, resource_name: 'my_resource')
62
+
63
+ expect(error.resource_name).to eql 'my_resource'
64
+ expect(error.association_name).to eql 'resource'
65
+ expect(error.association_id).to eql '123'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'erratum/errors/crud_errors/resource_not_found_error'
4
+ require 'active_record/errors'
5
+
6
+ class Erratum
7
+ module Errors
8
+ RSpec.describe ResourceNotFoundError do
9
+ it 'has a status of 404' do
10
+ error = ResourceNotFoundError.new
11
+
12
+ expect(error.http_status).to eql 404
13
+ end
14
+
15
+ it 'has a code' do
16
+ error = ResourceNotFoundError.new
17
+
18
+ expect(error.code).to eql 'errors.resource_not_found_error'
19
+ end
20
+
21
+ it 'has a title' do
22
+ error = ResourceNotFoundError.new
23
+
24
+ expect(error.title).to eql 'Resource Not Found'
25
+ end
26
+
27
+ it 'includes the resource name and action in the detail' do
28
+ error = ResourceNotFoundError.new resource_name: 'dragon',
29
+ action: 'ride'
30
+
31
+ expect(error.detail).to eql 'The dragon you attempted ' \
32
+ 'to ride for this request is either ' \
33
+ 'not authorized for the authenticated user ' \
34
+ 'or does not exist.'
35
+ end
36
+
37
+ it 'includes the resource name and action in the source' do
38
+ error = ResourceNotFoundError.new resource_name: 'dragon',
39
+ resource_id: 123
40
+
41
+ expect(error.source).to eql('dragon_id' => 123)
42
+ end
43
+
44
+ it 'can accept an array of IDs' do
45
+ error = ResourceNotFoundError.new resource_name: 'dragon',
46
+ resource_id: %w{123 456}
47
+
48
+ expect(error.source).to eql('dragon_id' => %w{123 456})
49
+ end
50
+
51
+ it 'can convert an "ActiveRecord::RecordNotFound" with no IDs' do
52
+ record_not_found_error = ActiveRecord::RecordNotFound.new \
53
+ "Couldn't find resource without an ID"
54
+ error = ResourceNotFoundError.convert(record_not_found_error)
55
+
56
+ expect(error.resource_name).to eql nil
57
+ expect(error.resource_id).to eql []
58
+ end
59
+
60
+ it 'can convert an "ActiveRecord::RecordNotFound" with one ID' do
61
+ record_not_found_error = ActiveRecord::RecordNotFound.new \
62
+ "Couldn't find resource with 'id'=123"
63
+ error = ResourceNotFoundError.convert(record_not_found_error)
64
+
65
+ expect(error.resource_name).to eql nil
66
+ expect(error.resource_id).to eql %w{123}
67
+ end
68
+
69
+ it 'can convert an "ActiveRecord::RecordNotFound" with multiple IDs' do
70
+ record_not_found_error = ActiveRecord::RecordNotFound.new \
71
+ "Couldn't find all resource with 'id': 123, 456, 789"
72
+ error = ResourceNotFoundError.convert(record_not_found_error)
73
+
74
+ expect(error.resource_name).to eql nil
75
+ expect(error.resource_id).to eql %w{123 456 789}
76
+ end
77
+
78
+ it 'can convert an "ActiveRecord::RecordNotFound" while overriding attributes' do
79
+ record_not_found_error = ActiveRecord::RecordNotFound.new \
80
+ "Couldn't find resource with 'id'=123"
81
+ error = ResourceNotFoundError.convert(record_not_found_error,
82
+ resource_name: 'my_resource')
83
+
84
+ expect(error.resource_name).to eql 'my_resource'
85
+ expect(error.resource_id).to eql %w{123}
86
+ end
87
+ end
88
+ end
89
+ end