powerhome-scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +708 -0
  4. data/Rakefile +16 -0
  5. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +257 -0
  6. data/app/controllers/scimitar/application_controller.rb +157 -0
  7. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  8. data/app/controllers/scimitar/resources_controller.rb +203 -0
  9. data/app/controllers/scimitar/schemas_controller.rb +21 -0
  10. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  11. data/app/models/scimitar/authentication_error.rb +9 -0
  12. data/app/models/scimitar/authentication_scheme.rb +18 -0
  13. data/app/models/scimitar/bulk.rb +8 -0
  14. data/app/models/scimitar/complex_types/address.rb +12 -0
  15. data/app/models/scimitar/complex_types/base.rb +83 -0
  16. data/app/models/scimitar/complex_types/email.rb +12 -0
  17. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  18. data/app/models/scimitar/complex_types/ims.rb +12 -0
  19. data/app/models/scimitar/complex_types/name.rb +12 -0
  20. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  21. data/app/models/scimitar/complex_types/photo.rb +12 -0
  22. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  23. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  24. data/app/models/scimitar/complex_types/role.rb +12 -0
  25. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  26. data/app/models/scimitar/engine_configuration.rb +32 -0
  27. data/app/models/scimitar/error_response.rb +32 -0
  28. data/app/models/scimitar/errors.rb +14 -0
  29. data/app/models/scimitar/filter.rb +11 -0
  30. data/app/models/scimitar/filter_error.rb +22 -0
  31. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  32. data/app/models/scimitar/lists/count.rb +64 -0
  33. data/app/models/scimitar/lists/query_parser.rb +745 -0
  34. data/app/models/scimitar/meta.rb +7 -0
  35. data/app/models/scimitar/not_found_error.rb +10 -0
  36. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  37. data/app/models/scimitar/resource_type.rb +29 -0
  38. data/app/models/scimitar/resources/base.rb +190 -0
  39. data/app/models/scimitar/resources/group.rb +13 -0
  40. data/app/models/scimitar/resources/mixin.rb +1524 -0
  41. data/app/models/scimitar/resources/user.rb +13 -0
  42. data/app/models/scimitar/schema/address.rb +25 -0
  43. data/app/models/scimitar/schema/attribute.rb +132 -0
  44. data/app/models/scimitar/schema/base.rb +90 -0
  45. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  46. data/app/models/scimitar/schema/email.rb +10 -0
  47. data/app/models/scimitar/schema/entitlement.rb +10 -0
  48. data/app/models/scimitar/schema/group.rb +27 -0
  49. data/app/models/scimitar/schema/ims.rb +10 -0
  50. data/app/models/scimitar/schema/name.rb +20 -0
  51. data/app/models/scimitar/schema/phone_number.rb +10 -0
  52. data/app/models/scimitar/schema/photo.rb +10 -0
  53. data/app/models/scimitar/schema/reference_group.rb +23 -0
  54. data/app/models/scimitar/schema/reference_member.rb +21 -0
  55. data/app/models/scimitar/schema/role.rb +10 -0
  56. data/app/models/scimitar/schema/user.rb +52 -0
  57. data/app/models/scimitar/schema/vdtp.rb +18 -0
  58. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  59. data/app/models/scimitar/service_provider_configuration.rb +60 -0
  60. data/app/models/scimitar/supportable.rb +14 -0
  61. data/app/views/layouts/scimitar/application.html.erb +14 -0
  62. data/config/initializers/scimitar.rb +111 -0
  63. data/config/routes.rb +6 -0
  64. data/lib/scimitar/engine.rb +63 -0
  65. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +216 -0
  66. data/lib/scimitar/support/utilities.rb +51 -0
  67. data/lib/scimitar/version.rb +13 -0
  68. data/lib/scimitar.rb +29 -0
  69. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  70. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  71. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  72. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  73. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  74. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  75. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  76. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  77. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  78. data/spec/apps/dummy/app/models/mock_user.rb +132 -0
  79. data/spec/apps/dummy/config/application.rb +18 -0
  80. data/spec/apps/dummy/config/boot.rb +2 -0
  81. data/spec/apps/dummy/config/environment.rb +2 -0
  82. data/spec/apps/dummy/config/environments/test.rb +38 -0
  83. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  84. data/spec/apps/dummy/config/initializers/scimitar.rb +61 -0
  85. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/apps/dummy/config/routes.rb +45 -0
  87. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +24 -0
  88. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  89. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +13 -0
  90. data/spec/apps/dummy/db/schema.rb +48 -0
  91. data/spec/controllers/scimitar/application_controller_spec.rb +296 -0
  92. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  93. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  94. data/spec/controllers/scimitar/schemas_controller_spec.rb +83 -0
  95. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  96. data/spec/models/scimitar/complex_types/address_spec.rb +18 -0
  97. data/spec/models/scimitar/complex_types/email_spec.rb +21 -0
  98. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  99. data/spec/models/scimitar/lists/query_parser_spec.rb +830 -0
  100. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  101. data/spec/models/scimitar/resources/base_spec.rb +485 -0
  102. data/spec/models/scimitar/resources/base_validation_spec.rb +86 -0
  103. data/spec/models/scimitar/resources/mixin_spec.rb +3562 -0
  104. data/spec/models/scimitar/resources/user_spec.rb +68 -0
  105. data/spec/models/scimitar/schema/attribute_spec.rb +99 -0
  106. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  107. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  108. data/spec/models/scimitar/schema/user_spec.rb +720 -0
  109. data/spec/requests/active_record_backed_resources_controller_spec.rb +1354 -0
  110. data/spec/requests/application_controller_spec.rb +61 -0
  111. data/spec/requests/controller_configuration_spec.rb +17 -0
  112. data/spec/requests/engine_spec.rb +45 -0
  113. data/spec/spec_helper.rb +101 -0
  114. data/spec/spec_helper_spec.rb +30 -0
  115. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +169 -0
  116. metadata +321 -0
@@ -0,0 +1,296 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ApplicationController do
4
+ context 'basic authentication' do
5
+ before do
6
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new(
7
+ basic_authenticator: Proc.new do | username, password |
8
+ username == 'A' && password == 'B'
9
+ end
10
+ )
11
+ end
12
+
13
+ controller do
14
+ rescue_from StandardError, with: :handle_resource_not_found
15
+
16
+ def index
17
+ render json: { 'message' => 'cool, cool!' }, format: :scim
18
+ end
19
+ end
20
+
21
+ it 'renders success when valid creds are given' do
22
+ request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('A', 'B')
23
+
24
+ get :index, params: { format: :scim }
25
+ expect(response).to be_ok
26
+ expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
27
+ expect(response.headers['WWW-Authenticate']).to eql('Basic')
28
+ end
29
+
30
+ it 'renders failure with bad password' do
31
+ request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('A', 'C')
32
+
33
+ get :index, params: { format: :scim }
34
+ expect(response).not_to be_ok
35
+ end
36
+
37
+ it 'renders failure with bad user name' do
38
+ request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('C', 'B')
39
+
40
+ get :index, params: { format: :scim }
41
+ expect(response).not_to be_ok
42
+ end
43
+
44
+ it 'renders failure with bad user name and password' do
45
+ request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('C', 'D')
46
+
47
+ get :index, params: { format: :scim }
48
+ expect(response).not_to be_ok
49
+ end
50
+
51
+ it 'renders failure with blank password' do
52
+ request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('A', '')
53
+
54
+ get :index, params: { format: :scim }
55
+ expect(response).not_to be_ok
56
+ end
57
+
58
+ it 'renders failure with missing header' do
59
+ get :index, params: { format: :scim }
60
+ expect(response).not_to be_ok
61
+ end
62
+ end
63
+
64
+ context 'token authentication' do
65
+ before do
66
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new(
67
+ token_authenticator: Proc.new do | token, options |
68
+ token == 'A'
69
+ end
70
+ )
71
+ end
72
+
73
+ controller do
74
+ rescue_from StandardError, with: :handle_resource_not_found
75
+
76
+ def index
77
+ render json: { 'message' => 'cool, cool!' }, format: :scim
78
+ end
79
+ end
80
+
81
+ it 'renders success when valid creds are given' do
82
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer A'
83
+
84
+ get :index, params: { format: :scim }
85
+ expect(response).to be_ok
86
+ expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
87
+ expect(response.headers['WWW-Authenticate']).to eql('Bearer')
88
+ end
89
+
90
+ it 'renders failure with bad token' do
91
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer Invalid'
92
+
93
+ get :index, params: { format: :scim }
94
+ expect(response).not_to be_ok
95
+ end
96
+
97
+ it 'renders failure with blank token' do
98
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer'
99
+
100
+ get :index, params: { format: :scim }
101
+ expect(response).not_to be_ok
102
+ end
103
+
104
+ it 'renders failure with missing header' do
105
+ get :index, params: { format: :scim }
106
+ expect(response).not_to be_ok
107
+ end
108
+ end
109
+
110
+ context 'authenticator evaluated within controller context' do
111
+
112
+ # Define a controller with a custom instance method 'valid_token'.
113
+ #
114
+ controller do
115
+ def index
116
+ render json: { 'message' => 'cool, cool!' }, format: :scim
117
+ end
118
+
119
+ def valid_token
120
+ 'B'
121
+ end
122
+ end
123
+
124
+ # Call the above controller method from the token authenticator Proc,
125
+ # proving that it was executed in the controller's context.
126
+ #
127
+ before do
128
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new(
129
+ token_authenticator: Proc.new do | token, options |
130
+ token == self.valid_token()
131
+ end
132
+ )
133
+ end
134
+
135
+ it 'renders success when valid creds are given' do
136
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer B'
137
+
138
+ get :index, params: { format: :scim }
139
+ expect(response).to be_ok
140
+ expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
141
+ expect(response.headers['WWW-Authenticate']).to eql('Bearer')
142
+ end
143
+
144
+ it 'renders failure with bad token' do
145
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer Invalid'
146
+
147
+ get :index, params: { format: :scim }
148
+ expect(response).not_to be_ok
149
+ end
150
+
151
+ it 'renders failure with blank token' do
152
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer'
153
+
154
+ get :index, params: { format: :scim }
155
+ expect(response).not_to be_ok
156
+ end
157
+
158
+ it 'renders failure with missing header' do
159
+ get :index, params: { format: :scim }
160
+ expect(response).not_to be_ok
161
+ end
162
+ end
163
+
164
+ context 'authenticated' do
165
+ controller do
166
+ rescue_from StandardError, with: :handle_resource_not_found
167
+
168
+ def index
169
+ render json: { 'message' => 'cool, cool!' }, format: :scim
170
+ end
171
+
172
+ def authenticated?
173
+ true
174
+ end
175
+ end
176
+
177
+ context 'authenticate' do
178
+ it 'renders index if authenticated' do
179
+ get :index, params: { format: :scim }
180
+ expect(response).to be_ok
181
+ expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
182
+ end
183
+
184
+ it 'renders not authorized response if not authenticated' do
185
+ allow(controller()).to receive(:authenticated?) { false }
186
+ get :index, params: { format: :scim }
187
+ expect(response).to have_http_status(:unauthorized)
188
+ parsed_body = JSON.parse(response.body)
189
+ expect(parsed_body).to include('schemas' => ['urn:ietf:params:scim:api:messages:2.0:Error'])
190
+ expect(parsed_body).to include('detail' => 'Requires authentication')
191
+ expect(parsed_body).to include('status' => '401')
192
+ end
193
+
194
+ it 'renders resource not found response when resource cannot be found for the given id' do
195
+ allow(controller()).to receive(:index).and_raise(StandardError)
196
+ get :index, params: { id: 10, format: :scim }
197
+ expect(response).to have_http_status(:not_found)
198
+ parsed_body = JSON.parse(response.body)
199
+ expect(parsed_body).to include('schemas' => ['urn:ietf:params:scim:api:messages:2.0:Error'])
200
+ expect(parsed_body).to include('detail' => 'Resource "10" not found')
201
+ expect(parsed_body).to include('status' => '404')
202
+ end
203
+ end
204
+ end
205
+
206
+ context 'error handling' do
207
+ controller do
208
+ def index
209
+ raise 'Bang'
210
+ end
211
+
212
+ def authenticated?
213
+ true
214
+ end
215
+ end
216
+
217
+ it 'handles general exceptions automatically' do
218
+ get :index, params: { format: :scim }
219
+
220
+ expect(response).to have_http_status(:internal_server_error)
221
+ parsed_body = JSON.parse(response.body)
222
+ expect(parsed_body).to include('schemas' => ['urn:ietf:params:scim:api:messages:2.0:Error'])
223
+ expect(parsed_body).to include('status' => '500')
224
+ expect(parsed_body).to include('detail' => 'Bang')
225
+ end
226
+
227
+ context 'with an exception reporter' do
228
+ around :each do | example |
229
+ original_configuration = Scimitar.engine_configuration.exception_reporter
230
+ Scimitar.engine_configuration.exception_reporter = Proc.new do | exception |
231
+ @exception = exception
232
+ end
233
+ example.run()
234
+ ensure
235
+ Scimitar.engine_configuration.exception_reporter = original_configuration
236
+ end
237
+
238
+ context 'and "internal server error"' do
239
+ it 'is invoked' do
240
+ get :index, params: { format: :scim }
241
+
242
+ expect(@exception).to be_a(RuntimeError)
243
+ expect(@exception.message).to eql('Bang')
244
+ end
245
+ end
246
+
247
+ context 'and "not found"' do
248
+ controller do
249
+ def index
250
+ handle_resource_not_found(ActiveRecord::RecordNotFound.new(42))
251
+ end
252
+ end
253
+
254
+ it 'is invoked' do
255
+ get :index, params: { format: :scim }
256
+
257
+ expect(@exception).to be_a(ActiveRecord::RecordNotFound)
258
+ expect(@exception.message).to eql('42')
259
+ end
260
+ end
261
+
262
+ context 'and bad JSON' do
263
+ controller do
264
+ def index
265
+ begin
266
+ raise 'Hello'
267
+ rescue
268
+ raise ActionDispatch::Http::Parameters::ParseError
269
+ end
270
+ end
271
+ end
272
+
273
+ it 'is invoked' do
274
+ get :index, params: { format: :scim }
275
+
276
+ expect(@exception).to be_a(ActionDispatch::Http::Parameters::ParseError)
277
+ expect(@exception.message).to eql('Hello')
278
+ end
279
+ end
280
+
281
+ context 'and a bad content type' do
282
+ controller do
283
+ def index; end
284
+ end
285
+
286
+ it 'is invoked' do
287
+ request.headers['Content-Type'] = 'text/plain'
288
+ get :index
289
+
290
+ expect(@exception).to be_a(Scimitar::ErrorResponse)
291
+ expect(@exception.message).to eql('Only application/scim+json type is accepted.')
292
+ end
293
+ end
294
+ end # "context 'exception reporter' do"
295
+ end # "context 'error handling' do"
296
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ResourceTypesController do
4
+ routes { Scimitar::Engine.routes }
5
+
6
+ before(:each) { allow(controller).to receive(:authenticated?).and_return(true) }
7
+
8
+ context 'GET index' do
9
+ it 'renders the resource type for user' do
10
+ get :index, format: :scim
11
+ response_hash = JSON.parse(response.body)
12
+ expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User', test: 1)),
13
+ Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group', test: 1))
14
+ ].to_json
15
+
16
+ response_hash = JSON.parse(response.body)
17
+ expect(response_hash).to eql(JSON.parse(expected_response))
18
+ end
19
+
20
+ context 'with custom resource types' do
21
+ around :each do | example |
22
+ example.run()
23
+ ensure
24
+ Scimitar::Engine.reset_custom_resources
25
+ end
26
+
27
+ it 'renders them' do
28
+ custom_resource = Class.new(Scimitar::Resources::Base) do
29
+ set_schema Scimitar::Schema::User
30
+
31
+ def self.endpoint
32
+ "/Gaga"
33
+ end
34
+
35
+ def self.resource_type_id
36
+ 'Gaga'
37
+ end
38
+ end
39
+
40
+ Scimitar::Engine.add_custom_resource(custom_resource)
41
+
42
+ get :index, params: { format: :scim }
43
+ response_hash = JSON.parse(response.body)
44
+ expect(response_hash.size).to eql(3)
45
+ end
46
+ end
47
+ end
48
+
49
+ context 'GET show' do
50
+ it 'renders the resource type for user' do
51
+ get :show, params: { name: 'User', format: :scim }
52
+ response_hash = JSON.parse(response.body)
53
+ expected_response = Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User')).to_json
54
+ expect(response_hash).to eql(JSON.parse(expected_response))
55
+ end
56
+
57
+ it 'renders the resource type for group' do
58
+ get :show, params: { name: 'Group', format: :scim }
59
+ response_hash = JSON.parse(response.body)
60
+ expected_response = Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group')).to_json
61
+ expect(response_hash).to eql(JSON.parse(expected_response))
62
+ end
63
+
64
+ it 'renders custom resource type' do
65
+ custom_resource = Class.new(Scimitar::Resources::Base) do
66
+ set_schema Scimitar::Schema::User
67
+
68
+ def self.endpoint
69
+ "/Gaga"
70
+ end
71
+
72
+ def self.resource_type_id
73
+ 'Gaga'
74
+ end
75
+ end
76
+
77
+ allow(Scimitar::Engine).to receive(:custom_resources) {[ custom_resource ]}
78
+
79
+ get :show, params: { name: 'Gaga', format: :scim }
80
+ response_hash = JSON.parse(response.body)
81
+ expected_response = custom_resource.resource_type(scim_resource_type_url(name: 'Gaga')).to_json
82
+ expect(response_hash).to eql(JSON.parse(expected_response))
83
+ end
84
+
85
+ it 'renders 404 if not recognised' do
86
+ get :show, params: { name: 'Foo', format: :scim }
87
+ expect(response).to have_http_status(:not_found)
88
+ response_hash = JSON.parse(response.body)
89
+ expect(response_hash['schemas']).to eql(['urn:ietf:params:scim:api:messages:2.0:Error'])
90
+ expect(response_hash['status' ]).to eql('404')
91
+ expect(response_hash['detail' ]).to eql('Resource "Foo" not found')
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,247 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ResourcesController do
4
+ class FakeGroup
5
+ include ActiveModel::Model
6
+
7
+ attr_accessor :scim_id
8
+ attr_accessor :display_name
9
+ attr_accessor :member_names
10
+
11
+ def self.scim_resource_type
12
+ return Scimitar::Resources::Group
13
+ end
14
+
15
+ def self.scim_attributes_map
16
+ return {
17
+ id: :id,
18
+ externalId: :scim_id,
19
+ displayName: :display_name,
20
+ members: :member_names
21
+ }
22
+ end
23
+
24
+ def self.scim_mutable_attributes
25
+ return nil
26
+ end
27
+
28
+ def self.scim_queryable_attributes
29
+ return { displayName: display_name }
30
+ end
31
+
32
+ include Scimitar::Resources::Mixin
33
+ end
34
+
35
+ let(:parsed_response) { JSON.parse(response.body, symbolize_names: true) }
36
+
37
+ before(:each) do
38
+ allow(controller()).to receive(:authenticated?).and_return(true)
39
+ allow(FakeGroup ).to receive(:all).and_return(double('ActiveRecord::Relation', where: []))
40
+ end
41
+
42
+ controller do
43
+ def index
44
+ super(FakeGroup.all) do | fake_group |
45
+ fake_group
46
+ end
47
+ end
48
+
49
+ def show
50
+ super do |id|
51
+ Scimitar::Resources::Group.new(id: id)
52
+ end
53
+ end
54
+
55
+ def create
56
+ super do |resource|
57
+ resource
58
+ end
59
+ end
60
+
61
+ def replace
62
+ super do |record_id, resource|
63
+ resource
64
+ end
65
+ end
66
+
67
+ # PATCH is more easily (and comprehensively) tested via
68
+ # spec/requests/active_record_backed_resources_controller_spec.rb.
69
+ #
70
+ def update # PATCH
71
+ raise NotImplementedError
72
+ end
73
+
74
+ def destroy
75
+ super do |id|
76
+ successful_delete?
77
+ end
78
+ end
79
+
80
+ def successful_delete? # Just a test hook
81
+ true
82
+ end
83
+
84
+ protected
85
+
86
+ def storage_class
87
+ FakeGroup
88
+ end
89
+ end
90
+
91
+ context 'GET show' do
92
+ it 'renders the resource' do
93
+ get :show, params: { id: '10', format: :scim }
94
+ expect(response.status).to eql(200)
95
+ expect(parsed_response()).to include(id: '10')
96
+ end
97
+ end
98
+
99
+ context 'POST create' do
100
+ it 'returns error if body is missing' do
101
+ post :create, params: { format: :scim }
102
+ expect(response.status).to eql(400)
103
+ expect(parsed_response()[:detail]).to eql('must provide a request body')
104
+ end
105
+
106
+ it 'works if the request is valid' do
107
+ post :create, params: { displayName: 'Sauron biz', format: :scim }
108
+ expect(response).to have_http_status(:created)
109
+ expect(parsed_response()[:displayName]).to eql('Sauron biz')
110
+ end
111
+
112
+ it 'renders error if resource object cannot be built from the params' do
113
+ @routes.draw do
114
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
115
+ end
116
+ put :replace, params: { id: 'group-id', name: {email: 'a@b.com'}, format: :scim }
117
+
118
+ expect(response.status).to eql(400)
119
+ expect(parsed_response()[:detail]).to match(/^Invalid/)
120
+ end
121
+
122
+ it 'renders application side error' do
123
+ expect_any_instance_of(Scimitar::Resources::Group).to receive(:to_json).and_raise(Scimitar::ErrorResponse.new(status: 400, detail: 'gaga'))
124
+
125
+ @routes.draw do
126
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
127
+ end
128
+ put :replace, params: { id: 'group-id', displayName: 'invalid name', format: :scim }
129
+
130
+ expect(response.status).to eql(400)
131
+ expect(parsed_response()[:detail]).to eql('gaga')
132
+ end
133
+
134
+ it 'renders externalId if provided' do
135
+ post :create, params: { externalId: 'some-id', displayName: 'sauron', format: :scim }
136
+
137
+ expect(response).to have_http_status(:created)
138
+
139
+ expect(parsed_response()[:displayName]).to eql('sauron')
140
+ expect(parsed_response()[:externalId]).to eql('some-id')
141
+ end
142
+
143
+ it 'maps internal NoMethodError failures to "Invalid request"' do
144
+ expect(controller()).to receive(:validate_request) { raise NoMethodError.new }
145
+
146
+ post :create, params: { externalId: 'some-id', displayName: 'sauron', format: :scim }
147
+
148
+ expect(response.status).to eql(400)
149
+ expect(parsed_response()[:detail]).to eql('Invalid request')
150
+ end
151
+ end
152
+
153
+ context 'PUT update' do
154
+ it 'returns error if body is missing' do
155
+ @routes.draw do
156
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
157
+ end
158
+ put :replace, params: { id: 'group-id', format: :scim }
159
+
160
+ expect(response.status).to eql(400)
161
+ expect(parsed_response()[:detail]).to eql('must provide a request body')
162
+ end
163
+
164
+ it 'works if the request is valid' do
165
+ @routes.draw do
166
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
167
+ end
168
+ put :replace, params: { id: 'group-id', displayName: 'sauron', format: :scim }
169
+
170
+ expect(response.status).to eql(200)
171
+ expect(parsed_response()[:displayName]).to eql('sauron')
172
+ end
173
+
174
+ it 'renders error if resource object cannot be built from the params' do
175
+ @routes.draw do
176
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
177
+ end
178
+ put :replace, params: { id: 'group-id', name: {email: 'a@b.com'}, format: :scim }
179
+
180
+ expect(response.status).to eql(400)
181
+ expect(parsed_response()[:detail]).to match(/^Invalid/)
182
+ end
183
+
184
+ it 'renders application side error' do
185
+ allow_any_instance_of(Scimitar::Resources::Group).to receive(:to_json).and_raise(Scimitar::ErrorResponse.new(status: 400, detail: 'gaga'))
186
+
187
+ @routes.draw do
188
+ put 'scimitar/resources/:id', action: 'replace', controller: 'scimitar/resources'
189
+ end
190
+ put :replace, params: { id: 'group-id', displayName: 'invalid name', format: :scim }
191
+
192
+ expect(response.status).to eql(400)
193
+ expect(parsed_response()[:detail]).to eql('gaga')
194
+ end
195
+
196
+ end
197
+
198
+ context 'DELETE destroy' do
199
+ it 'returns an empty response with no content status if deletion is successful' do
200
+ delete :destroy, params: { id: 'group-id', format: :scim }
201
+ expect(response).to have_http_status(:no_content)
202
+ expect(response.body).to be_empty
203
+ end
204
+
205
+ it 'renders error if deletion fails' do
206
+ allow(controller()).to receive(:successful_delete?).and_return(false)
207
+ delete :destroy, params: { id: 'group-id', format: :scim }
208
+ expect(response).to have_http_status(:internal_server_error)
209
+ expect(parsed_response()[:detail]).to eql("Failed to delete the resource with id 'group-id'. Please try again later.")
210
+ end
211
+ end
212
+
213
+ context 'service methods' do
214
+ context '#scim_pagination_info' do
215
+ it 'applies defaults' do
216
+ result = controller().send(:scim_pagination_info)
217
+
218
+ expect(result.limit).to eql(Scimitar.service_provider_configuration(location: nil).filter.maxResults)
219
+ expect(result.offset).to eql(0)
220
+ expect(result.start_index).to eql(1)
221
+ expect(result.total).to be_nil
222
+ end
223
+
224
+ it 'reads parameters' do
225
+ allow(controller()).to receive(:params).and_return({count: '10', startIndex: '5'})
226
+
227
+ result = controller().send(:scim_pagination_info)
228
+
229
+ expect(result.limit).to eql(10)
230
+ expect(result.offset).to eql(4)
231
+ expect(result.start_index).to eql(5)
232
+ end
233
+
234
+ it 'accepts an up-front total' do
235
+ result = controller().send(:scim_pagination_info, 150)
236
+
237
+ expect(result.total).to eql(150)
238
+ end
239
+ end # "context '#scim_pagination_info' do"
240
+
241
+ context '#storage_class' do
242
+ it 'raises "not implemented" to warn subclass authors' do
243
+ expect { described_class.new.send(:storage_class) }.to raise_error(NotImplementedError)
244
+ end
245
+ end # "context '#storage_class' do"
246
+ end # "context 'service methods' do"
247
+ end