Skip to content

Commit 1e36143

Browse files
Fix PATCH conflicts when meta.version is present.
At present all PATCH requests for resources that have a `meta.version` attribute will fail with ErrConflict because the PatchService.Do() method incorrectly mutates the current resource instead of its replacement. This error was not caught by existing unit tests because they do not set a `meta.version` attribute and db.Memory().Replace() has the following check: version := ref.MetaVersionOrEmpty() if len(version) > 0 && m.db[id].MetaVersionOrEmpty() != version { return spec.ErrConflict } This has the effect of permitting replacements when there is no version present -- which is the case for the existing unit tests **but not real APIs**. Simply adding a `meta.version` attribute to unit tests (as in this commit) yields failures like as the following: === RUN TestPatchService/TestDo/patch_to_make_a_difference patch_test.go:99: Error Trace: patch_test.go:99 patch_test.go:366 Error: Expected nil, but got: &spec.Error{Status:412, Type:"conflict"} Test: TestPatchService/TestDo/patch_to_make_a_difference An identical fix was originally proposed by @zakirhussain in imulab#80. This commit expands on that work to include unit test changes to catch a regression and (hopefully) a better explanation on the underlying cause of the issue. Co-authored-by: Zakir Hussain <pro.zakir24@gmail.com> Signed-off-by: Aaron Jacobs <aaron.jacobs@rstudio.com>
1 parent fec7838 commit 1e36143

File tree

2 files changed

+43
-3
lines changed

2 files changed

+43
-3
lines changed

pkg/v2/service/patch.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ func (s *patchService) Do(ctx context.Context, req *PatchRequest) (resp *PatchRe
8383
return
8484
}
8585

86-
resource, err := s.database.Get(ctx, req.ResourceID, nil)
86+
ref, err := s.database.Get(ctx, req.ResourceID, nil)
8787
if err != nil {
8888
return
8989
}
9090

9191
if s.config.ETag.Supported && req.MatchCriteria != nil {
92-
if !req.MatchCriteria(resource) {
92+
if !req.MatchCriteria(ref) {
9393
err = fmt.Errorf("%w: resource does not meet pre condition", spec.ErrConflict)
9494
return
9595
}
@@ -98,7 +98,7 @@ func (s *patchService) Do(ctx context.Context, req *PatchRequest) (resp *PatchRe
9898
// To save another database round trip, we use Clone to retain independent copy of the fetched resource.
9999
// However, because the cloned instance share subscribers, it is better to work on the original instance.
100100
// Hence, we assign reference to the clone, which will not be modified.
101-
ref := resource.Clone()
101+
resource := ref.Clone()
102102

103103
for _, f := range s.preFilters {
104104
if err = f.FilterRef(ctx, resource, ref); err != nil {

pkg/v2/service/patch_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ func (s *PatchServiceTestSuite) TestDo() {
4343
err := database.Insert(context.TODO(), s.resourceOf(t, map[string]interface{}{
4444
"schemas": []interface{}{"urn:ietf:params:scim:schemas:core:2.0:User"},
4545
"id": "foo",
46+
"meta": map[string]interface{}{
47+
"resourceType": "User",
48+
"created": "2019-11-20T13:09:00",
49+
"lastModified": "2019-11-20T13:09:00",
50+
"location": "https://identity.imulab.io/Users/foo",
51+
"version": "W/\"1\"",
52+
},
4653
"userName": "foo",
4754
"timezone": "Asia/Shanghai",
4855
"emails": []interface{}{
@@ -90,6 +97,7 @@ func (s *PatchServiceTestSuite) TestDo() {
9097
},
9198
expect: func(t *testing.T, resp *PatchResponse, err error) {
9299
assert.Nil(t, err)
100+
require.NotNil(t, resp)
93101
assert.True(t, resp.Patched)
94102
assert.NotEmpty(t, resp.Resource.MetaVersionOrEmpty())
95103
assert.NotEqual(t, resp.Ref.MetaVersionOrEmpty(), resp.Resource.MetaVersionOrEmpty())
@@ -105,6 +113,13 @@ func (s *PatchServiceTestSuite) TestDo() {
105113
err := database.Insert(context.TODO(), s.resourceOf(t, map[string]interface{}{
106114
"schemas": []interface{}{"urn:ietf:params:scim:schemas:core:2.0:User"},
107115
"id": "foo",
116+
"meta": map[string]interface{}{
117+
"resourceType": "User",
118+
"created": "2019-11-20T13:09:00",
119+
"lastModified": "2019-11-20T13:09:00",
120+
"location": "https://identity.imulab.io/Users/foo",
121+
"version": "W/\"1\"",
122+
},
108123
"userName": "foo",
109124
"timezone": "Asia/Shanghai",
110125
"emails": []interface{}{
@@ -143,6 +158,7 @@ func (s *PatchServiceTestSuite) TestDo() {
143158
},
144159
expect: func(t *testing.T, resp *PatchResponse, err error) {
145160
assert.Nil(t, err)
161+
require.NotNil(t, resp)
146162
assert.False(t, resp.Patched)
147163
},
148164
},
@@ -153,6 +169,13 @@ func (s *PatchServiceTestSuite) TestDo() {
153169
err := database.Insert(context.TODO(), s.resourceOf(t, map[string]interface{}{
154170
"schemas": []interface{}{"urn:ietf:params:scim:schemas:core:2.0:User"},
155171
"id": "foo",
172+
"meta": map[string]interface{}{
173+
"resourceType": "User",
174+
"created": "2019-11-20T13:09:00",
175+
"lastModified": "2019-11-20T13:09:00",
176+
"location": "https://identity.imulab.io/Users/foo",
177+
"version": "W/\"1\"",
178+
},
156179
"userName": "foo",
157180
"timezone": "Asia/Shanghai",
158181
"emails": []interface{}{
@@ -200,6 +223,7 @@ func (s *PatchServiceTestSuite) TestDo() {
200223
},
201224
expect: func(t *testing.T, resp *PatchResponse, err error) {
202225
assert.Nil(t, err)
226+
require.NotNil(t, resp)
203227
assert.True(t, resp.Patched)
204228
assert.NotEmpty(t, resp.Resource.MetaVersionOrEmpty())
205229
assert.NotEqual(t, resp.Ref.MetaVersionOrEmpty(), resp.Resource.MetaVersionOrEmpty())
@@ -215,6 +239,13 @@ func (s *PatchServiceTestSuite) TestDo() {
215239
err := database.Insert(context.TODO(), s.resourceOf(t, map[string]interface{}{
216240
"schemas": []interface{}{"urn:ietf:params:scim:schemas:core:2.0:User"},
217241
"id": "foo",
242+
"meta": map[string]interface{}{
243+
"resourceType": "User",
244+
"created": "2019-11-20T13:09:00",
245+
"lastModified": "2019-11-20T13:09:00",
246+
"location": "https://identity.imulab.io/Users/foo",
247+
"version": "W/\"1\"",
248+
},
218249
"userName": "foo",
219250
"timezone": "Asia/Shanghai",
220251
"emails": []interface{}{
@@ -255,6 +286,7 @@ func (s *PatchServiceTestSuite) TestDo() {
255286
},
256287
expect: func(t *testing.T, resp *PatchResponse, err error) {
257288
assert.Nil(t, err)
289+
require.NotNil(t, resp)
258290
assert.True(t, resp.Patched)
259291
assert.Equal(t, "bar", resp.Resource.Navigator().Dot("userName").Current().Raw())
260292
assert.Nil(t, bcrypt.CompareHashAndPassword(
@@ -273,6 +305,13 @@ func (s *PatchServiceTestSuite) TestDo() {
273305
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
274306
},
275307
"id": "foo",
308+
"meta": map[string]interface{}{
309+
"resourceType": "User",
310+
"created": "2019-11-20T13:09:00",
311+
"lastModified": "2019-11-20T13:09:00",
312+
"location": "https://identity.imulab.io/Users/foo",
313+
"version": "W/\"1\"",
314+
},
276315
"userName": "foo",
277316
"emails": []interface{}{
278317
map[string]interface{}{
@@ -313,6 +352,7 @@ func (s *PatchServiceTestSuite) TestDo() {
313352
},
314353
expect: func(t *testing.T, resp *PatchResponse, err error) {
315354
assert.Nil(t, err)
355+
require.NotNil(t, resp)
316356
assert.True(t, resp.Patched)
317357
assert.Equal(t, "6546579", resp.Resource.Navigator().Dot("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User").Dot("employeeNumber").Current().Raw())
318358
},

0 commit comments

Comments
 (0)