Skip to content

Commit 16dd57c

Browse files
Dragomir-Ivanovsmyrman
authored andcommitted
Fix PUT,PATCH on read-only fields
Fields stored in non-scalar form by `Storer` will differ from incoming JSON scalar form. Input scalars need to be converted to non-scalar form before comparison, so decision to be made if the field has changed.
1 parent ca07988 commit 16dd57c

File tree

2 files changed

+92
-4
lines changed

2 files changed

+92
-4
lines changed

rest/method_item_put_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,3 +582,84 @@ func TestPutItemRequiredDefault(t *testing.T) {
582582
t.Run(n, tc.Test)
583583
}
584584
}
585+
586+
func TestPutItemReadOnly(t *testing.T) {
587+
now := time.Now()
588+
yesterday := now.Add(-24 * time.Hour)
589+
590+
timeOldStr := "2018-01-03T00:00:00+02:00"
591+
timeOld, _ := time.Parse(time.RFC3339, timeOldStr)
592+
593+
sharedInit := func() *requestTestVars {
594+
s1 := mem.NewHandler()
595+
s1.Insert(context.Background(), []*resource.Item{
596+
{ID: "1", ETag: "a", Updated: yesterday, Payload: map[string]interface{}{"id": "1", "foo": "odd"}},
597+
{ID: "2", ETag: "b", Updated: yesterday, Payload: map[string]interface{}{"id": "2", "foo": "odd", "zar": "old"}},
598+
// Storer will persist `schema.Time{}` as `time.Time` type.
599+
{ID: "3", ETag: "c", Updated: yesterday, Payload: map[string]interface{}{"id": "3", "foo": "odd", "tar": timeOld}},
600+
})
601+
602+
idx := resource.NewIndex()
603+
idx.Bind("foo", schema.Schema{
604+
Fields: schema.Fields{
605+
"id": {Sortable: true, Filterable: true},
606+
"foo": {Filterable: true},
607+
"zar": {ReadOnly: true},
608+
"tar": {ReadOnly: true, Validator: &schema.Time{}},
609+
},
610+
}, s1, resource.DefaultConf)
611+
612+
return &requestTestVars{
613+
Index: idx,
614+
Storers: map[string]resource.Storer{"foo": s1},
615+
}
616+
}
617+
618+
tests := map[string]requestTest{
619+
`put:read-only:string:new`: {
620+
Init: sharedInit,
621+
NewRequest: func() (*http.Request, error) {
622+
body := bytes.NewReader([]byte(`{"foo": "baz", "zar":"old"}`))
623+
return http.NewRequest("PUT", `/foo/1`, body)
624+
},
625+
ResponseCode: http.StatusUnprocessableEntity,
626+
ResponseBody: `{"code":422,"issues":{"zar":["read-only"]},"message":"Document contains error(s)"}`,
627+
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "odd"}),
628+
},
629+
`put:read-only:string:old`: {
630+
Init: sharedInit,
631+
NewRequest: func() (*http.Request, error) {
632+
body := bytes.NewReader([]byte(`{"foo": "baz", "zar":"old"}`))
633+
return http.NewRequest("PUT", `/foo/2`, body)
634+
},
635+
ResponseCode: http.StatusOK,
636+
ResponseBody: `{"foo":"baz","id":"2","zar":"old"}`,
637+
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz", "zar": "old"}),
638+
},
639+
`put:read-only:time:old`: {
640+
Init: sharedInit,
641+
NewRequest: func() (*http.Request, error) {
642+
body := bytes.NewReader([]byte(`{"foo": "odd", "tar":"2018-01-03T00:00:00+02:00"}`))
643+
return http.NewRequest("PUT", `/foo/3`, body)
644+
},
645+
ResponseCode: http.StatusOK,
646+
ResponseBody: `{"foo":"odd","id":"3","tar":"2018-01-03T00:00:00+02:00"}`,
647+
ExtraTest: checkPayload("foo", "3", map[string]interface{}{"id": "3", "foo": "odd", "tar": timeOld}),
648+
},
649+
`put:read-only:time:new`: {
650+
Init: sharedInit,
651+
NewRequest: func() (*http.Request, error) {
652+
body := bytes.NewReader([]byte(`{"foo": "odd", "tar":"2018-01-03T11:11:11+02:00"}`))
653+
return http.NewRequest("PUT", `/foo/3`, body)
654+
},
655+
ResponseCode: http.StatusUnprocessableEntity,
656+
ResponseBody: `{"code":422,"issues":{"tar":["read-only"]},"message":"Document contains error(s)"}`,
657+
ExtraTest: checkPayload("foo", "3", map[string]interface{}{"id": "3", "foo": "odd", "tar": timeOld}),
658+
},
659+
}
660+
661+
for n, tc := range tests {
662+
tc := tc // capture range variable
663+
t.Run(n, tc.Test)
664+
}
665+
}

schema/schema.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,17 @@ func (s Schema) Prepare(ctx context.Context, payload map[string]interface{}, ori
121121
// Handle prepare on an updated document (original provided).
122122
oValue, oFound := (*original)[field]
123123
// Apply value to change-set only if the field was not identical same in the original doc.
124-
if found && (!oFound || !reflect.DeepEqual(value, oValue)) {
125-
changes[field] = value
126-
}
127-
if !found && oFound && replace {
124+
if found {
125+
if def.Validator != nil {
126+
if validated, err := def.Validator.Validate(value); err != nil {
127+
changes[field] = value
128+
} else if !oFound || !reflect.DeepEqual(validated, oValue) {
129+
changes[field] = validated
130+
}
131+
} else if !oFound || !reflect.DeepEqual(value, oValue) {
132+
changes[field] = value
133+
}
134+
} else if oFound && replace {
128135
// When replace arg is true and a field is not present in the payload but is in the original,
129136
// the tombstone value is set on the field in the change map so validator can enforce the
130137
// ReadOnly and then the field can be removed from the output document.

0 commit comments

Comments
 (0)