Skip to content

Commit

Permalink
Prevent removing of (Required,Default) fields in HTTP PUT. Fixes #174
Browse files Browse the repository at this point in the history
  • Loading branch information
Dragomir-Ivanov committed Nov 27, 2018
1 parent 5003f4f commit d4a819c
Show file tree
Hide file tree
Showing 2 changed files with 263 additions and 19 deletions.
278 changes: 260 additions & 18 deletions rest/method_item_put_test.go
Expand Up @@ -14,11 +14,8 @@ import (
"github.com/rs/rest-layer/schema/query"
)

func TestPutItem(t *testing.T) {
now := time.Now()
yesterday := now.Add(-24 * time.Hour)

sharedInit := func() *requestTestVars {
func initForBasic(now, yesterday time.Time) func() *requestTestVars {
return func() *requestTestVars {
s1 := mem.NewHandler()
s1.Insert(context.Background(), []*resource.Item{
{ID: "1", ETag: "a", Updated: now, Payload: map[string]interface{}{"id": "1", "foo": "odd", "bar": "baz"}},
Expand Down Expand Up @@ -50,6 +47,78 @@ func TestPutItem(t *testing.T) {
Storers: map[string]resource.Storer{"foo": s1, "foo.sub": s2},
}
}
}

func initForDefaultField(now, yesterday time.Time) func() *requestTestVars {
return func() *requestTestVars {
s1 := mem.NewHandler()
s1.Insert(context.Background(), []*resource.Item{
{ID: "1", ETag: "a", Updated: now, Payload: map[string]interface{}{"id": "1", "foo": "odd"}},
{ID: "2", ETag: "b", Updated: now, Payload: map[string]interface{}{"id": "2", "foo": "odd", "bar": "value"}},
})
idx := resource.NewIndex()
idx.Bind("foo", schema.Schema{
Fields: schema.Fields{
"id": {Sortable: true, Filterable: true},
"foo": {Filterable: true},
"bar": {Filterable: true, Default: "default"},
},
}, s1, resource.DefaultConf)
return &requestTestVars{
Index: idx,
Storers: map[string]resource.Storer{"foo": s1},
}
}
}

func initForRequiredField(now, yesterday time.Time) func() *requestTestVars {
return func() *requestTestVars {
s1 := mem.NewHandler()
s1.Insert(context.Background(), []*resource.Item{
{ID: "1", ETag: "a", Updated: now, Payload: map[string]interface{}{"id": "1", "foo": "odd"}},
{ID: "2", ETag: "b", Updated: now, Payload: map[string]interface{}{"id": "2", "foo": "odd", "bar": "original"}},
})
idx := resource.NewIndex()
idx.Bind("foo", schema.Schema{
Fields: schema.Fields{
"id": {Sortable: true, Filterable: true},
"foo": {Filterable: true},
"bar": {Filterable: true, Required: true},
},
}, s1, resource.DefaultConf)
return &requestTestVars{
Index: idx,
Storers: map[string]resource.Storer{"foo": s1},
}
}
}

func initForRequiredDefaultField(now, yesterday time.Time) func() *requestTestVars {
return func() *requestTestVars {
s1 := mem.NewHandler()
s1.Insert(context.Background(), []*resource.Item{
{ID: "1", ETag: "a", Updated: now, Payload: map[string]interface{}{"id": "1", "foo": "odd"}},
{ID: "2", ETag: "b", Updated: now, Payload: map[string]interface{}{"id": "2", "foo": "odd", "bar": "original"}},
})
idx := resource.NewIndex()
idx.Bind("foo", schema.Schema{
Fields: schema.Fields{
"id": {Sortable: true, Filterable: true},
"foo": {Filterable: true},
"bar": {Filterable: true, Required: true, Default: "default"},
},
}, s1, resource.DefaultConf)
return &requestTestVars{
Index: idx,
Storers: map[string]resource.Storer{"foo": s1},
}
}
}

func TestPutItem(t *testing.T) {
now := time.Now()
yesterday := now.Add(-24 * time.Hour)

checkPayload := func(name string, id interface{}, payload map[string]interface{}) requestCheckerFunc {
return func(t *testing.T, vars *requestTestVars) {
var item *resource.Item
Expand Down Expand Up @@ -119,7 +188,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "bar"}),
},
`pathID:not-found,body:valid`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", `/foo/66`, body)
Expand All @@ -129,7 +198,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "66", map[string]interface{}{"id": "66", "foo": "baz"}),
},
`pathID:found,body:invalid-json`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`invalid`))
return http.NewRequest("PUT", "/foo/2", body)
Expand All @@ -142,7 +211,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "even", "bar": "baz"}),
},
`pathID:found,body:invalid-field`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"invalid": true}`))
return http.NewRequest("PUT", "/foo/2", body)
Expand All @@ -158,7 +227,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "even", "bar": "baz"}),
},
`pathID:found,body:alter-id`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"id": "3"}`))
return http.NewRequest("PUT", "/foo/2", body)
Expand All @@ -171,7 +240,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "even", "bar": "baz"}),
},
`pathID:found,body:valid`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/2", body)
Expand All @@ -181,7 +250,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz"}),
},
`pathID:found,body:valid,fields:invalid`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/2?fields=invalid", body)
Expand All @@ -197,7 +266,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "even", "bar": "baz"}),
},
`pathID:found,body:valid,fields:valid`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/2?fields=foo", body)
Expand All @@ -207,7 +276,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz"}),
},
`pathID:found,body:valid,header["If-Match"]:not-matching`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
r, err := http.NewRequest("PUT", "/foo/2", body)
Expand All @@ -222,7 +291,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "even", "bar": "baz"}),
},
`pathID:found,body:valid,header["If-Match"]:matching`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
r, err := http.NewRequest("PUT", "/foo/2", body)
Expand All @@ -237,7 +306,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz"}),
},
`pathID:found,body:valid,header["If-Unmodified-Since"]:invalid`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
r, err := http.NewRequest("PUT", "/foo/1", body)
Expand All @@ -252,7 +321,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "odd", "bar": "baz"}),
},
`pathID:found,body:valid,header["If-Unmodified-Since"]:not-matching`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
r, err := http.NewRequest("PUT", "/foo/1", body)
Expand All @@ -267,7 +336,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "odd", "bar": "baz"}),
},
`parentPathID:found,pathID:found,body:alter-parent-id`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "2"}`))
r, err := http.NewRequest("PUT", "/foo/3/sub/1", body)
Expand All @@ -281,7 +350,7 @@ func TestPutItem(t *testing.T) {
ExtraTest: checkPayload("foo.sub", "1", map[string]interface{}{"id": "1", "foo": "2"}),
},
`parentPathID:found,pathID:found,body:no-parent-id`: {
Init: sharedInit,
Init: initForBasic(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{}`))
r, err := http.NewRequest("PUT", "/foo/3/sub/1", body)
Expand All @@ -294,6 +363,179 @@ func TestPutItem(t *testing.T) {
ResponseBody: `{"id": "1", "foo": "3"}`,
ExtraTest: checkPayload("foo.sub", "1", map[string]interface{}{"id": "1", "foo": "3"}),
},

`pathID:not-found,body:valid,default:missing`: {
Init: initForDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/66", body)
},
ResponseCode: http.StatusCreated,
ResponseBody: `{"id": "66", "foo": "baz", "bar": "default"}`,
ExtraTest: checkPayload("foo", "66", map[string]interface{}{"id": "66", "foo": "baz", "bar": "default"}),
},
`pathID:not-found,body:valid,default:set`: {
Init: initForDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz", "bar": "value"}`))
return http.NewRequest("PUT", "/foo/66", body)
},
ResponseCode: http.StatusCreated,
ResponseBody: `{"id": "66", "foo": "baz", "bar": "value"}`,
ExtraTest: checkPayload("foo", "66", map[string]interface{}{"id": "66", "foo": "baz", "bar": "value"}),
},
`pathID:found,body:valid,default:missing`: {
Init: initForDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/1", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "1", "foo": "baz"}`,
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "baz"}),
},
`pathID:found,body:valid,default:set`: {
Init: initForDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz", "bar": "value"}`))
return http.NewRequest("PUT", "/foo/1", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "1", "foo": "baz", "bar": "value"}`,
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "baz", "bar": "value"}),
},
`pathID:found,body:valid,default:delete`: {
Init: initForDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/2", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "2", "foo": "baz"}`,
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz"}),
},

`pathID:not-found,body:valid,required:missing`: {
Init: initForRequiredField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/66", body)
},
ResponseCode: http.StatusUnprocessableEntity,
ResponseBody: `{
"code": 422,
"message": "Document contains error(s)",
"issues": {
"bar": ["required"]
}
}`,
},
`pathID:not-found,body:valid,required:set`: {
Init: initForRequiredField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz", "bar": "value"}`))
return http.NewRequest("PUT", "/foo/1", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "1", "foo": "baz", "bar": "value"}`,
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "baz", "bar": "value"}),
},
`pathID:found,body:valid,required:missing`: {
Init: initForRequiredField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/1", body)
},
ResponseCode: http.StatusUnprocessableEntity,
ResponseBody: `{
"code": 422,
"message": "Document contains error(s)",
"issues": {
"bar": ["required"]
}
}`,
},
`pathID:found,body:valid,required:change`: {
Init: initForRequiredField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz", "bar": "value1"}`))
return http.NewRequest("PUT", "/foo/2", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "2", "foo": "baz", "bar": "value1"}`,
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz", "bar": "value1"}),
},
`pathID:found,body:valid,required:delete`: {
Init: initForRequiredField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/2", body)
},
ResponseCode: http.StatusUnprocessableEntity,
ResponseBody: `{
"code": 422,
"message": "Document contains error(s)",
"issues": {
"bar": ["required"]
}
}`,
},

`pathID:not-found,body:valid,required-default:missing`: {
Init: initForRequiredDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/66", body)
},
ResponseCode: http.StatusCreated,
ResponseBody: `{"id": "66", "foo": "baz", "bar": "default"}`,
ExtraTest: checkPayload("foo", "66", map[string]interface{}{"id": "66", "foo": "baz", "bar": "default"}),
},
`pathID:not-found,body:valid,required-default:set`: {
Init: initForRequiredDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz", "bar": "value"}`))
return http.NewRequest("PUT", "/foo/1", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "1", "foo": "baz", "bar": "value"}`,
ExtraTest: checkPayload("foo", "1", map[string]interface{}{"id": "1", "foo": "baz", "bar": "value"}),
},
`pathID:found,body:valid,required-default:missing`: {
Init: initForRequiredDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/1", body)
},
ResponseCode: http.StatusUnprocessableEntity,
ResponseBody: `{
"code": 422,
"message": "Document contains error(s)",
"issues": {
"bar": ["required"]
}
}`,
},
`pathID:found,body:valid,required-default:change`: {
Init: initForRequiredDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz", "bar": "value"}`))
return http.NewRequest("PUT", "/foo/2", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "2", "foo": "baz", "bar": "value"}`,
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz", "bar": "value"}),
},
`pathID:found,body:valid,required-default:delete`: {
Init: initForRequiredDefaultField(now, yesterday),
NewRequest: func() (*http.Request, error) {
body := bytes.NewReader([]byte(`{"foo": "baz"}`))
return http.NewRequest("PUT", "/foo/2", body)
},
ResponseCode: http.StatusOK,
ResponseBody: `{"id": "2", "foo": "baz", "bar": "default"}`,
ExtraTest: checkPayload("foo", "2", map[string]interface{}{"id": "2", "foo": "baz", "bar": "default"}),
},
}

for n, tc := range tests {
Expand Down

0 comments on commit d4a819c

Please sign in to comment.