scimitar 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +710 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +51 -16
- data/app/controllers/scimitar/application_controller.rb +13 -4
- data/app/models/scimitar/complex_types/address.rb +0 -6
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resources/mixin.rb +4 -1
- data/lib/scimitar/support/utilities.rb +51 -0
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +1 -0
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/config/routes.rb +15 -3
- data/spec/apps/dummy/db/schema.rb +2 -2
- data/spec/controllers/scimitar/application_controller_spec.rb +56 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +1 -1
- data/spec/models/scimitar/complex_types/address_spec.rb +3 -4
- data/spec/models/scimitar/resources/mixin_spec.rb +22 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +410 -50
- data/spec/spec_helper.rb +8 -0
- metadata +34 -11
@@ -60,12 +60,20 @@ module Scimitar
|
|
60
60
|
|
61
61
|
# POST (create)
|
62
62
|
#
|
63
|
-
|
63
|
+
# Calls #save! on the new record if no block is given, else invokes the
|
64
|
+
# block, passing it the new ActiveRecord model instance to be saved. It
|
65
|
+
# is up to the block to make any further changes and persist the record.
|
66
|
+
#
|
67
|
+
# Blocks are invoked from within a wrapping database transaction.
|
68
|
+
# ActiveRecord::RecordInvalid exceptions are handled for you, rendering
|
69
|
+
# an appropriate SCIM error.
|
70
|
+
#
|
71
|
+
def create(&block)
|
64
72
|
super do |scim_resource|
|
65
73
|
self.storage_class().transaction do
|
66
74
|
record = self.storage_class().new
|
67
75
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
68
|
-
self.save!(record)
|
76
|
+
self.save!(record, &block)
|
69
77
|
record_to_scim(record)
|
70
78
|
end
|
71
79
|
end
|
@@ -73,12 +81,16 @@ module Scimitar
|
|
73
81
|
|
74
82
|
# PUT (replace)
|
75
83
|
#
|
76
|
-
|
84
|
+
# Calls #save! on the updated record if no block is given, else invokes the
|
85
|
+
# block, passing the updated record which the block must persist, with the
|
86
|
+
# same rules as for #create.
|
87
|
+
#
|
88
|
+
def replace(&block)
|
77
89
|
super do |record_id, scim_resource|
|
78
90
|
self.storage_class().transaction do
|
79
91
|
record = self.find_record(record_id)
|
80
92
|
record.from_scim!(scim_hash: scim_resource.as_json())
|
81
|
-
self.save!(record)
|
93
|
+
self.save!(record, &block)
|
82
94
|
record_to_scim(record)
|
83
95
|
end
|
84
96
|
end
|
@@ -86,12 +98,16 @@ module Scimitar
|
|
86
98
|
|
87
99
|
# PATCH (update)
|
88
100
|
#
|
89
|
-
|
101
|
+
# Calls #save! on the updated record if no block is given, else invokes the
|
102
|
+
# block, passing the updated record which the block must persist, with the
|
103
|
+
# same rules as for #create.
|
104
|
+
#
|
105
|
+
def update(&block)
|
90
106
|
super do |record_id, patch_hash|
|
91
107
|
self.storage_class().transaction do
|
92
108
|
record = self.find_record(record_id)
|
93
109
|
record.from_scim_patch!(patch_hash: patch_hash)
|
94
|
-
self.save!(record)
|
110
|
+
self.save!(record, &block)
|
95
111
|
record_to_scim(record)
|
96
112
|
end
|
97
113
|
end
|
@@ -134,6 +150,17 @@ module Scimitar
|
|
134
150
|
raise NotImplementedError
|
135
151
|
end
|
136
152
|
|
153
|
+
# Return an Array of exceptions that #save! can rescue and handle with a
|
154
|
+
# SCIM error automatically.
|
155
|
+
#
|
156
|
+
def scimitar_rescuable_exceptions
|
157
|
+
[
|
158
|
+
ActiveRecord::RecordInvalid,
|
159
|
+
ActiveRecord::RecordNotSaved,
|
160
|
+
ActiveRecord::RecordNotUnique,
|
161
|
+
]
|
162
|
+
end
|
163
|
+
|
137
164
|
# Find a record by ID. Subclasses can override this if they need special
|
138
165
|
# lookup behaviour.
|
139
166
|
#
|
@@ -173,17 +200,25 @@ module Scimitar
|
|
173
200
|
else
|
174
201
|
record.save!
|
175
202
|
end
|
176
|
-
rescue
|
177
|
-
|
203
|
+
rescue *self.scimitar_rescuable_exceptions() => exception
|
204
|
+
handle_on_save_exception(record, exception)
|
178
205
|
end
|
179
206
|
|
180
|
-
# Deal with
|
181
|
-
# error.
|
207
|
+
# Deal with exceptions related to errors upon saving, by responding with
|
208
|
+
# an appropriate SCIM error. This is most effective if the record has
|
209
|
+
# validation errors defined, but falls back to the provided exception's
|
210
|
+
# message otherwise.
|
182
211
|
#
|
183
|
-
# +record+::
|
212
|
+
# +record+:: The record that provoked the exception. Mandatory.
|
213
|
+
# +exception+:: The exception that was raised. If omitted, a default of
|
214
|
+
# 'Unknown', in English with no I18n, is used.
|
184
215
|
#
|
185
|
-
def
|
186
|
-
|
216
|
+
def handle_on_save_exception(record, exception = RuntimeError.new('Unknown'))
|
217
|
+
details = if record.errors.present?
|
218
|
+
record.errors.full_messages.join('; ')
|
219
|
+
else
|
220
|
+
exception.message
|
221
|
+
end
|
187
222
|
|
188
223
|
# https://tools.ietf.org/html/rfc7644#page-12
|
189
224
|
#
|
@@ -193,14 +228,14 @@ module Scimitar
|
|
193
228
|
# status code 409 (Conflict) with a "scimType" error code of
|
194
229
|
# "uniqueness"
|
195
230
|
#
|
196
|
-
if record.errors.any? { | e | e.type == :taken }
|
231
|
+
if exception.is_a?(ActiveRecord::RecordNotUnique) || record.errors.any? { | e | e.type == :taken }
|
197
232
|
raise Scimitar::ErrorResponse.new(
|
198
233
|
status: 409,
|
199
234
|
scimType: 'uniqueness',
|
200
|
-
detail:
|
235
|
+
detail: "Operation failed due to a uniqueness constraint: #{details}"
|
201
236
|
)
|
202
237
|
else
|
203
|
-
raise Scimitar::ResourceInvalidError.new(
|
238
|
+
raise Scimitar::ResourceInvalidError.new(details)
|
204
239
|
end
|
205
240
|
end
|
206
241
|
|
@@ -124,8 +124,13 @@ module Scimitar
|
|
124
124
|
#
|
125
125
|
# https://stackoverflow.com/questions/10239970/what-is-the-delimiter-for-www-authenticate-for-multiple-schemes
|
126
126
|
#
|
127
|
-
response.set_header('
|
128
|
-
response.set_header('
|
127
|
+
response.set_header('WWW-Authenticate', 'Basic' ) if Scimitar.engine_configuration.basic_authenticator.present?
|
128
|
+
response.set_header('WWW-Authenticate', 'Bearer') if Scimitar.engine_configuration.token_authenticator.present?
|
129
|
+
|
130
|
+
# No matter what a caller might request via headers, the only content
|
131
|
+
# type we can ever respond with is JSON-for-SCIM.
|
132
|
+
#
|
133
|
+
response.set_header('Content-Type', "#{Mime::Type.lookup_by_extension(:scim)}; charset=utf-8")
|
129
134
|
end
|
130
135
|
|
131
136
|
def authenticate
|
@@ -134,11 +139,15 @@ module Scimitar
|
|
134
139
|
|
135
140
|
def authenticated?
|
136
141
|
result = if Scimitar.engine_configuration.basic_authenticator.present?
|
137
|
-
authenticate_with_http_basic
|
142
|
+
authenticate_with_http_basic do |username, password|
|
143
|
+
instance_exec(username, password, &Scimitar.engine_configuration.basic_authenticator)
|
144
|
+
end
|
138
145
|
end
|
139
146
|
|
140
147
|
result ||= if Scimitar.engine_configuration.token_authenticator.present?
|
141
|
-
authenticate_with_http_token
|
148
|
+
authenticate_with_http_token do |token, options|
|
149
|
+
instance_exec(token, options, &Scimitar.engine_configuration.token_authenticator)
|
150
|
+
end
|
142
151
|
end
|
143
152
|
|
144
153
|
return result
|
@@ -2,7 +2,7 @@ module Scimitar
|
|
2
2
|
class ResourceInvalidError < ErrorResponse
|
3
3
|
|
4
4
|
def initialize(error_message)
|
5
|
-
super(status: 400, scimType: 'invalidValue', detail:"Operation failed since record has become invalid: #{error_message}")
|
5
|
+
super(status: 400, scimType: 'invalidValue', detail: "Operation failed since record has become invalid: #{error_message}")
|
6
6
|
end
|
7
7
|
|
8
8
|
end
|
@@ -952,7 +952,10 @@ module Scimitar
|
|
952
952
|
|
953
953
|
when 'replace'
|
954
954
|
if path_component == 'root'
|
955
|
-
|
955
|
+
dot_pathed_value = value.inject({}) do |hash, (k, v)|
|
956
|
+
hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v))
|
957
|
+
end
|
958
|
+
altering_hash[path_component].deep_merge!(dot_pathed_value)
|
956
959
|
else
|
957
960
|
altering_hash[path_component] = value
|
958
961
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Scimitar
|
2
|
+
|
3
|
+
# Namespace containing various chunks of Scimitar support code that don't
|
4
|
+
# logically fit into other areas.
|
5
|
+
#
|
6
|
+
module Support
|
7
|
+
|
8
|
+
# A namespace that contains various stand-alone utility methods which act
|
9
|
+
# as helpers for other parts of the code base, without risking namespace
|
10
|
+
# pollution by e.g. being part of a module loaded into a client class.
|
11
|
+
#
|
12
|
+
module Utilities
|
13
|
+
|
14
|
+
# Takes an array of components that usually come from a dotted path such
|
15
|
+
# as <tt>foo.bar.baz</tt>, along with a value that is found at the end of
|
16
|
+
# that path, then converts it into a nested Hash with each level of the
|
17
|
+
# Hash corresponding to a step along the path.
|
18
|
+
#
|
19
|
+
# This was written to help with edge case SCIM uses where (most often, at
|
20
|
+
# least) inbound calls use a dotted notation where nested values are more
|
21
|
+
# commonly accepted; converting to nesting makes it easier for subsequent
|
22
|
+
# processing code, which needs only handle nested Hash data.
|
23
|
+
#
|
24
|
+
# As an example, passing:
|
25
|
+
#
|
26
|
+
# ['foo', 'bar', 'baz'], 'value'
|
27
|
+
#
|
28
|
+
# ...yields:
|
29
|
+
#
|
30
|
+
# {'foo' => {'bar' => {'baz' => 'value'}}}
|
31
|
+
#
|
32
|
+
# Parameters:
|
33
|
+
#
|
34
|
+
# +array+:: Array containing path components, usually acquired from a
|
35
|
+
# string with dot separators and a call to String#split.
|
36
|
+
#
|
37
|
+
# +value+:: The value found at the path indicated by +array+.
|
38
|
+
#
|
39
|
+
# If +array+ is empty, +value+ is returned directly, with no nesting
|
40
|
+
# Hash wrapping it.
|
41
|
+
#
|
42
|
+
def self.dot_path(array, value)
|
43
|
+
return value if array.empty?
|
44
|
+
|
45
|
+
{}.tap do | hash |
|
46
|
+
hash[array.shift()] = self.dot_path(array, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/scimitar/version.rb
CHANGED
@@ -3,11 +3,11 @@ module Scimitar
|
|
3
3
|
# Gem version. If this changes, be sure to re-run "bundle install" or
|
4
4
|
# "bundle update".
|
5
5
|
#
|
6
|
-
VERSION = '1.
|
6
|
+
VERSION = '1.8.0'
|
7
7
|
|
8
8
|
# Date for VERSION. If this changes, be sure to re-run "bundle install"
|
9
9
|
# or "bundle update".
|
10
10
|
#
|
11
|
-
DATE = '
|
11
|
+
DATE = '2024-01-15'
|
12
12
|
|
13
13
|
end
|
data/lib/scimitar.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
# For tests only - uses custom 'create' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#create.
|
3
|
+
#
|
4
|
+
class CustomCreateMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
OVERRIDDEN_NAME = SecureRandom.uuid
|
7
|
+
|
8
|
+
def create
|
9
|
+
super do | resource |
|
10
|
+
resource.first_name = OVERRIDDEN_NAME
|
11
|
+
resource.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def storage_class
|
18
|
+
MockUser
|
19
|
+
end
|
20
|
+
|
21
|
+
def storage_scope
|
22
|
+
MockUser.all
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# For tests only - uses custom 'replace' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#create.
|
3
|
+
#
|
4
|
+
class CustomReplaceMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
OVERRIDDEN_NAME = SecureRandom.uuid
|
7
|
+
|
8
|
+
def replace
|
9
|
+
super do | resource |
|
10
|
+
resource.first_name = OVERRIDDEN_NAME
|
11
|
+
resource.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def storage_class
|
18
|
+
MockUser
|
19
|
+
end
|
20
|
+
|
21
|
+
def storage_scope
|
22
|
+
MockUser.all
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# For tests only - uses custom 'update' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#create.
|
3
|
+
#
|
4
|
+
class CustomUpdateMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
OVERRIDDEN_NAME = SecureRandom.uuid
|
7
|
+
|
8
|
+
def update
|
9
|
+
super do | resource |
|
10
|
+
resource.first_name = OVERRIDDEN_NAME
|
11
|
+
resource.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def storage_class
|
18
|
+
MockUser
|
19
|
+
end
|
20
|
+
|
21
|
+
def storage_scope
|
22
|
+
MockUser.all
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -17,14 +17,26 @@ Rails.application.routes.draw do
|
|
17
17
|
get 'Groups/:id', to: 'mock_groups#show'
|
18
18
|
patch 'Groups/:id', to: 'mock_groups#update'
|
19
19
|
|
20
|
-
# For testing blocks passed to ActiveRecordBackedResourcesController#
|
20
|
+
# For testing blocks passed to ActiveRecordBackedResourcesController#create,
|
21
|
+
# #update, #replace and #destroy.
|
21
22
|
#
|
23
|
+
post 'CustomCreateUsers', to: 'custom_create_mock_users#create'
|
24
|
+
patch 'CustomUpdateUsers/:id', to: 'custom_update_mock_users#update'
|
25
|
+
put 'CustomReplaceUsers/:id', to: 'custom_replace_mock_users#replace'
|
22
26
|
delete 'CustomDestroyUsers/:id', to: 'custom_destroy_mock_users#destroy'
|
23
27
|
|
28
|
+
# Needed because the auto-render of most of the above includes a 'url_for'
|
29
|
+
# call for a 'show' action, so we must include routes (implemented in the
|
30
|
+
# base class) for the "show" endpoint.
|
31
|
+
#
|
32
|
+
get 'CustomCreateUsers/:id', to: 'custom_create_mock_users#show'
|
33
|
+
get 'CustomUpdateUsers/:id', to: 'custom_update_mock_users#show'
|
34
|
+
get 'CustomReplaceUsers/:id', to: 'custom_replace_mock_users#show'
|
35
|
+
|
24
36
|
# For testing blocks passed to ActiveRecordBackedResourcesController#save!
|
25
37
|
#
|
26
|
-
post 'CustomSaveUsers',
|
27
|
-
get
|
38
|
+
post 'CustomSaveUsers', to: 'custom_save_mock_users#create'
|
39
|
+
get 'CustomSaveUsers/:id', to: 'custom_save_mock_users#show'
|
28
40
|
|
29
41
|
# For testing environment inside Scimitar::ApplicationController subclasses.
|
30
42
|
#
|
@@ -30,8 +30,8 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do
|
|
30
30
|
end
|
31
31
|
|
32
32
|
create_table "mock_users", primary_key: "primary_key", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
33
|
-
t.datetime "created_at",
|
34
|
-
t.datetime "updated_at",
|
33
|
+
t.datetime "created_at", null: false
|
34
|
+
t.datetime "updated_at", null: false
|
35
35
|
t.text "scim_uid"
|
36
36
|
t.text "username"
|
37
37
|
t.text "password"
|
@@ -24,7 +24,7 @@ RSpec.describe Scimitar::ApplicationController do
|
|
24
24
|
get :index, params: { format: :scim }
|
25
25
|
expect(response).to be_ok
|
26
26
|
expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
|
27
|
-
expect(response.headers['
|
27
|
+
expect(response.headers['WWW-Authenticate']).to eql('Basic')
|
28
28
|
end
|
29
29
|
|
30
30
|
it 'renders failure with bad password' do
|
@@ -84,7 +84,61 @@ RSpec.describe Scimitar::ApplicationController do
|
|
84
84
|
get :index, params: { format: :scim }
|
85
85
|
expect(response).to be_ok
|
86
86
|
expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
|
87
|
-
expect(response.headers['
|
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')
|
88
142
|
end
|
89
143
|
|
90
144
|
it 'renders failure with bad token' do
|
@@ -27,7 +27,7 @@ RSpec.describe Scimitar::SchemasController do
|
|
27
27
|
expect(parsed_body['name']).to eql('User')
|
28
28
|
end
|
29
29
|
|
30
|
-
it 'includes the controller
|
30
|
+
it 'includes the controller customised schema location' do
|
31
31
|
get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
|
32
32
|
expect(response).to be_ok
|
33
33
|
parsed_body = JSON.parse(response.body)
|
@@ -2,8 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
RSpec.describe Scimitar::ComplexTypes::Address do
|
4
4
|
context '#as_json' do
|
5
|
-
it 'assumes
|
6
|
-
expect(described_class.new.as_json).to eq(
|
5
|
+
it 'assumes no defaults' do
|
6
|
+
expect(described_class.new.as_json).to eq({})
|
7
7
|
end
|
8
8
|
|
9
9
|
it 'allows a custom address type' do
|
@@ -11,9 +11,8 @@ RSpec.describe Scimitar::ComplexTypes::Address do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
it 'shows the set address' do
|
14
|
-
expect(described_class.new(country: 'NZ').as_json).to eq('
|
14
|
+
expect(described_class.new(country: 'NZ').as_json).to eq('country' => 'NZ')
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
end
|
19
|
-
|
@@ -2717,6 +2717,28 @@ RSpec.describe Scimitar::Resources::Mixin do
|
|
2717
2717
|
expect(@instance.username).to eql('1234')
|
2718
2718
|
end
|
2719
2719
|
|
2720
|
+
it 'which updates nested values using root syntax' do
|
2721
|
+
@instance.update!(first_name: 'Foo', last_name: 'Bar')
|
2722
|
+
|
2723
|
+
path = 'name.givenName'
|
2724
|
+
path = path.upcase if force_upper_case
|
2725
|
+
|
2726
|
+
patch = {
|
2727
|
+
'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
|
2728
|
+
'Operations' => [
|
2729
|
+
{
|
2730
|
+
'op' => 'replace',
|
2731
|
+
'value' => {
|
2732
|
+
path => 'Baz'
|
2733
|
+
}
|
2734
|
+
}
|
2735
|
+
]
|
2736
|
+
}
|
2737
|
+
|
2738
|
+
@instance.from_scim_patch!(patch_hash: patch)
|
2739
|
+
expect(@instance.first_name).to eql('Baz')
|
2740
|
+
end
|
2741
|
+
|
2720
2742
|
it 'which updates nested values' do
|
2721
2743
|
@instance.update!(first_name: 'Foo', last_name: 'Bar')
|
2722
2744
|
|