feat: url encoded parameters (uri + form) are only considered a slice when multiple values are set

- if `?a=123`, "123" is the value that can be validated as string, int, etc
 - if `?a=123&a=456`, the slice []type{123,456} is the value that can be validated as slice of strings, ints, etc.
This commit is contained in:
Adrien Marquès 2021-06-22 23:06:15 +02:00
parent fcc8b39717
commit 1cc24be254
2 changed files with 121 additions and 117 deletions

View File

@ -66,17 +66,24 @@ func (i *T) GetQuery(req http.Request) error {
query := req.URL.Query()
for name, param := range i.service.Query {
value, exist := query[name]
if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
values, exist := query[name]
if !exist {
if !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
continue
}
parsed := parseParameter(value)
var parsed interface{}
// consider element instead of slice or elements when only 1
if len(values) == 1 {
parsed = parseParameter(values[0])
} else { // consider slice
parsed = parseParameter(values)
}
cast, valid := param.Validator(parsed)
if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType)
@ -108,6 +115,13 @@ func (i *T) GetForm(req http.Request) error {
return i.parseMultipart(req)
default:
// fail on at least 1 mandatory form param when there is no body
for name, param := range i.service.Form {
if !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
}
return nil
}
}
@ -119,21 +133,19 @@ func (i *T) parseJSON(req http.Request) error {
decoder := json.NewDecoder(req.Body)
err := decoder.Decode(&parsed)
if err == io.EOF {
return nil
}
if err != io.EOF {
if err != nil {
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
}
}
for name, param := range i.service.Form {
value, exist := parsed[name]
if !exist && !param.Optional {
if !exist {
if !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
if !exist {
continue
}
@ -155,17 +167,24 @@ func (i *T) parseUrlencoded(req http.Request) error {
}
for name, param := range i.service.Form {
value, exist := req.PostForm[name]
if !exist && !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
values, exist := req.PostForm[name]
if !exist {
if !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
continue
}
parsed := parseParameter(value)
var parsed interface{}
// consider element instead of slice or elements when only 1
if len(values) == 1 {
parsed = parseParameter(values[0])
} else { // consider slice
parsed = parseParameter(values)
}
cast, valid := param.Validator(parsed)
if !valid {
return fmt.Errorf("%s: %w", name, ErrInvalidType)
@ -196,11 +215,10 @@ func (i *T) parseMultipart(req http.Request) error {
for name, param := range i.service.Form {
component, exist := mpr.Data[name]
if !exist && !param.Optional {
if !exist {
if !param.Optional {
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
}
if !exist {
continue
}

View File

@ -135,13 +135,11 @@ func TestStoreWithUri(t *testing.T) {
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
t.FailNow()
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
}
return
}
t.Errorf("unexpected error <%s>", err)
t.FailNow()
t.Fatalf("unexpected error <%s>", err)
}
if len(store.Data) != len(service.Input) {
@ -183,14 +181,14 @@ func TestExtractQuery(t *testing.T) {
Query: "a",
Err: nil,
ParamNames: []string{"a"},
ParamValues: [][]string{[]string{""}},
ParamValues: [][]string{{""}},
},
{
ServiceParam: []string{"a"},
Query: "a&b",
Err: nil,
ParamNames: []string{"a"},
ParamValues: [][]string{[]string{""}},
ParamValues: [][]string{{""}},
},
{
ServiceParam: []string{"a", "missing"},
@ -204,40 +202,40 @@ func TestExtractQuery(t *testing.T) {
Query: "a&b",
Err: nil,
ParamNames: []string{"a", "b"},
ParamValues: [][]string{[]string{""}, []string{""}},
ParamValues: [][]string{{""}, {""}},
},
{
ServiceParam: []string{"a"},
Err: nil,
Query: "a=",
ParamNames: []string{"a"},
ParamValues: [][]string{[]string{""}},
ParamValues: [][]string{{""}},
},
{
ServiceParam: []string{"a", "b"},
Err: nil,
Query: "a=&b=x",
ParamNames: []string{"a", "b"},
ParamValues: [][]string{[]string{""}, []string{"x"}},
ParamValues: [][]string{{""}, {"x"}},
},
{
ServiceParam: []string{"a", "c"},
Err: nil,
Query: "a=b&c=d",
ParamNames: []string{"a", "c"},
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
ParamValues: [][]string{{"b"}, {"d"}},
},
{
ServiceParam: []string{"a", "c"},
Err: nil,
Query: "a=b&c=d&a=x",
ParamNames: []string{"a", "c"},
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
ParamValues: [][]string{{"b", "x"}, {"d"}},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
t.Run(fmt.Sprintf("request[%d]", i), func(t *testing.T) {
store := New(getServiceWithQuery(test.ServiceParam...))
@ -246,19 +244,16 @@ func TestExtractQuery(t *testing.T) {
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
t.FailNow()
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
}
return
}
t.Errorf("unexpected error <%s>", err)
t.FailNow()
t.Fatalf("unexpected error <%s>", err)
}
if test.ParamNames == nil || test.ParamValues == nil {
if len(store.Data) != 0 {
t.Errorf("expected no GET parameters and got %d", len(store.Data))
t.FailNow()
t.Fatalf("expected no GET parameters and got %d", len(store.Data))
}
// no param to check
@ -266,8 +261,7 @@ func TestExtractQuery(t *testing.T) {
}
if len(test.ParamNames) != len(test.ParamValues) {
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
t.FailNow()
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
}
for pi, pName := range test.ParamNames {
@ -276,29 +270,35 @@ func TestExtractQuery(t *testing.T) {
t.Run(pName, func(t *testing.T) {
param, isset := store.Data[pName]
if !isset {
t.Errorf("param does not exist")
t.FailNow()
t.Fatalf("param does not exist")
}
// single value, should return a single element
if len(values) == 1 {
cast, canCast := param.(string)
if !canCast {
t.Fatalf("should return a string (got '%v')", cast)
}
if values[0] != cast {
t.Fatalf("should return '%s' (got '%s')", values[0], cast)
}
return
}
// multiple values, should return a slice
cast, canCast := param.([]interface{})
if !canCast {
t.Errorf("should return a []string (got '%v')", cast)
t.FailNow()
t.Fatalf("should return a []string (got '%v')", cast)
}
if len(cast) != len(values) {
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
t.FailNow()
t.Fatalf("should return %d string(s) (got '%d')", len(values), len(cast))
}
for vi, value := range values {
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
if value != cast[vi] {
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
t.FailNow()
t.Fatalf("should return '%s' (got '%s')", value, cast[vi])
}
})
}
})
@ -326,9 +326,7 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
store := New(nil)
err := store.GetForm(*req)
if err == nil {
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
t.FailNow()
t.Fatalf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
}
}
func TestExtractFormUrlEncoded(t *testing.T) {
@ -359,14 +357,14 @@ func TestExtractFormUrlEncoded(t *testing.T) {
URLEncoded: "a",
Err: nil,
ParamNames: []string{"a"},
ParamValues: [][]string{[]string{""}},
ParamValues: [][]string{{""}},
},
{
ServiceParams: []string{"a"},
URLEncoded: "a&b",
Err: nil,
ParamNames: []string{"a"},
ParamValues: [][]string{[]string{""}},
ParamValues: [][]string{{""}},
},
{
ServiceParams: []string{"a", "missing"},
@ -380,35 +378,35 @@ func TestExtractFormUrlEncoded(t *testing.T) {
URLEncoded: "a&b",
Err: nil,
ParamNames: []string{"a", "b"},
ParamValues: [][]string{[]string{""}, []string{""}},
ParamValues: [][]string{{""}, {""}},
},
{
ServiceParams: []string{"a"},
Err: nil,
URLEncoded: "a=",
ParamNames: []string{"a"},
ParamValues: [][]string{[]string{""}},
ParamValues: [][]string{{""}},
},
{
ServiceParams: []string{"a", "b"},
Err: nil,
URLEncoded: "a=&b=x",
ParamNames: []string{"a", "b"},
ParamValues: [][]string{[]string{""}, []string{"x"}},
ParamValues: [][]string{{""}, {"x"}},
},
{
ServiceParams: []string{"a", "c"},
Err: nil,
URLEncoded: "a=b&c=d",
ParamNames: []string{"a", "c"},
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
ParamValues: [][]string{{"b"}, {"d"}},
},
{
ServiceParams: []string{"a", "c"},
Err: nil,
URLEncoded: "a=b&c=d&a=x",
ParamNames: []string{"a", "c"},
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
ParamValues: [][]string{{"b", "x"}, {"d"}},
},
}
@ -424,19 +422,16 @@ func TestExtractFormUrlEncoded(t *testing.T) {
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
t.FailNow()
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
}
return
}
t.Errorf("unexpected error <%s>", err)
t.FailNow()
t.Fatalf("unexpected error <%s>", err)
}
if test.ParamNames == nil || test.ParamValues == nil {
if len(store.Data) != 0 {
t.Errorf("expected no GET parameters and got %d", len(store.Data))
t.FailNow()
t.Fatalf("expected no GET parameters and got %d", len(store.Data))
}
// no param to check
@ -444,8 +439,7 @@ func TestExtractFormUrlEncoded(t *testing.T) {
}
if len(test.ParamNames) != len(test.ParamValues) {
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
t.FailNow()
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
}
for pi, key := range test.ParamNames {
@ -454,29 +448,35 @@ func TestExtractFormUrlEncoded(t *testing.T) {
t.Run(key, func(t *testing.T) {
param, isset := store.Data[key]
if !isset {
t.Errorf("param does not exist")
t.FailNow()
t.Fatalf("param does not exist")
}
// single value, should return a single element
if len(values) == 1 {
cast, canCast := param.(string)
if !canCast {
t.Fatalf("should return a string (got '%v')", cast)
}
if values[0] != cast {
t.Fatalf("should return '%s' (got '%s')", values[0], cast)
}
return
}
// multiple values, should return a slice
cast, canCast := param.([]interface{})
if !canCast {
t.Errorf("should return a []interface{} (got '%v')", cast)
t.FailNow()
t.Fatalf("should return a []string (got '%v')", cast)
}
if len(cast) != len(values) {
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
t.FailNow()
t.Fatalf("should return %d string(s) (got '%d')", len(values), len(cast))
}
for vi, value := range values {
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
if value != cast[vi] {
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
t.FailNow()
t.Fatalf("should return '%s' (got '%s')", value, cast[vi])
}
})
}
})
@ -567,19 +567,16 @@ func TestJsonParameters(t *testing.T) {
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
t.FailNow()
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
}
return
}
t.Errorf("unexpected error <%s>", err)
t.FailNow()
t.Fatalf("unexpected error <%s>", err)
}
if test.ParamNames == nil || test.ParamValues == nil {
if len(store.Data) != 0 {
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
t.FailNow()
t.Fatalf("expected no JSON parameters and got %d", len(store.Data))
}
// no param to check
@ -587,8 +584,7 @@ func TestJsonParameters(t *testing.T) {
}
if len(test.ParamNames) != len(test.ParamValues) {
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
t.FailNow()
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
}
for pi, pName := range test.ParamNames {
@ -599,8 +595,7 @@ func TestJsonParameters(t *testing.T) {
param, isset := store.Data[key]
if !isset {
t.Errorf("store should contain element with key '%s'", key)
t.FailNow()
t.Fatalf("store should contain element with key '%s'", key)
return
}
@ -610,13 +605,11 @@ func TestJsonParameters(t *testing.T) {
paramValueType := reflect.TypeOf(param)
if valueType != paramValueType {
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
t.FailNow()
t.Fatalf("should be of type %v (got '%v')", valueType, paramValueType)
}
if paramValue != value {
t.Errorf("should return %v (got '%v')", value, paramValue)
t.FailNow()
t.Fatalf("should return %v (got '%v')", value, paramValue)
}
})
@ -724,19 +717,16 @@ x
if err != nil {
if test.Err != nil {
if !errors.Is(err, test.Err) {
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
t.FailNow()
t.Fatalf("expected error <%s>, got <%s>", test.Err, err)
}
return
}
t.Errorf("unexpected error <%s>", err)
t.FailNow()
t.Fatalf("unexpected error <%s>", err)
}
if test.ParamNames == nil || test.ParamValues == nil {
if len(store.Data) != 0 {
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
t.FailNow()
t.Fatalf("expected no JSON parameters and got %d", len(store.Data))
}
// no param to check
@ -744,8 +734,7 @@ x
}
if len(test.ParamNames) != len(test.ParamValues) {
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
t.FailNow()
t.Fatalf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
}
for pi, key := range test.ParamNames {
@ -755,8 +744,7 @@ x
param, isset := store.Data[key]
if !isset {
t.Errorf("store should contain element with key '%s'", key)
t.FailNow()
t.Fatalf("store should contain element with key '%s'", key)
return
}
@ -766,13 +754,11 @@ x
paramValueType := reflect.TypeOf(param)
if valueType != paramValueType {
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
t.FailNow()
t.Fatalf("should be of type %v (got '%v')", valueType, paramValueType)
}
if paramValue != value {
t.Errorf("should return %v (got '%v')", value, paramValue)
t.FailNow()
t.Fatalf("should return %v (got '%v')", value, paramValue)
}
})