1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-24 09:02:26 +02:00

backported some of the v0.23.0 form validators, fixes and tests

This commit is contained in:
Gani Georgiev 2024-10-18 16:03:49 +03:00
parent e92d3cccd0
commit cbc88d7a01
5 changed files with 100 additions and 22 deletions

View File

@ -19,5 +19,6 @@ func NewRealtimeSubscribe() *RealtimeSubscribe {
func (form *RealtimeSubscribe) Validate() error { func (form *RealtimeSubscribe) Validate() error {
return validation.ValidateStruct(form, return validation.ValidateStruct(form,
validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)), validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)),
validation.Field(&form.Subscriptions, validation.Length(0, 1000)),
) )
} }

View File

@ -1,33 +1,86 @@
package forms_test package forms_test
import ( import (
"encoding/json"
"fmt"
"strings" "strings"
"testing" "testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/forms" "github.com/pocketbase/pocketbase/forms"
) )
func TestRealtimeSubscribeValidate(t *testing.T) { func TestRealtimeSubscribeValidate(t *testing.T) {
t.Parallel() t.Parallel()
scenarios := []struct { validSubscriptionsLimit := make([]string, 1000)
clientId string for i := 0; i < len(validSubscriptionsLimit); i++ {
expectError bool validSubscriptionsLimit[i] = fmt.Sprintf(`"%d"`, i)
}{ }
{"", true}, invalidSubscriptionsLimit := make([]string, 1001)
{strings.Repeat("a", 256), true}, for i := 0; i < len(invalidSubscriptionsLimit); i++ {
{"test", false}, invalidSubscriptionsLimit[i] = fmt.Sprintf(`"%d"`, i)
} }
for i, s := range scenarios { scenarios := []struct {
form := forms.NewRealtimeSubscribe() name string
form.ClientId = s.clientId data string
expectedErrors []string
}{
{
"empty data",
`{}`,
[]string{"clientId"},
},
{
"clientId > max chars limit",
`{"clientId":"` + strings.Repeat("a", 256) + `"}`,
[]string{"clientId"},
},
{
"clientId <= max chars limit",
`{"clientId":"` + strings.Repeat("a", 255) + `"}`,
[]string{},
},
{
"subscriptions > max limit",
`{"clientId":"test", "subscriptions":[` + strings.Join(invalidSubscriptionsLimit, ",") + `]}`,
[]string{"subscriptions"},
},
{
"subscriptions <= max limit",
`{"clientId":"test", "subscriptions":[` + strings.Join(validSubscriptionsLimit, ",") + `]}`,
[]string{},
},
}
err := form.Validate() for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
form := forms.NewRealtimeSubscribe()
hasErr := err != nil err := json.Unmarshal([]byte(s.data), &form)
if hasErr != s.expectError { if err != nil {
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) t.Fatal(err)
} }
result := form.Validate()
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Fatalf("Failed to parse errors %v", result)
return
}
// check errors
if len(errs) > len(s.expectedErrors) {
t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs)
}
for _, k := range s.expectedErrors {
if _, ok := errs[k]; !ok {
t.Fatalf("Missing expected error key %q in %v", k, errs)
}
}
})
} }
} }

View File

@ -2,6 +2,7 @@ package validators
import ( import (
"fmt" "fmt"
"math"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
@ -159,6 +160,10 @@ func (validator *RecordDataValidator) checkNumberValue(field *schema.SchemaField
return nil // nothing to check (skip zero-defaults) return nil // nothing to check (skip zero-defaults)
} }
if math.IsInf(val, 0) || math.IsNaN(val) {
return validation.NewError("validation_nan", "The submitted number is not properly formatted")
}
options, _ := field.Options.(*schema.NumberOptions) options, _ := field.Options.(*schema.NumberOptions)
if options.NoDecimal && val != float64(int64(val)) { if options.NoDecimal && val != float64(int64(val)) {

View File

@ -248,6 +248,16 @@ func TestRecordDataValidatorValidateNumber(t *testing.T) {
nil, nil,
[]string{"field2"}, []string{"field2"},
}, },
{
"(number) check infinities and NaN",
map[string]any{
"field1": "Inf",
"field2": "-Inf",
"field4": "NaN",
},
nil,
[]string{"field1", "field2", "field4"},
},
{ {
"(number) check min constraint", "(number) check min constraint",
map[string]any{ map[string]any{

View File

@ -6,10 +6,11 @@ import (
"io" "io"
"net/http" "net/http"
"reflect" "reflect"
"regexp"
"strconv"
"strings" "strings"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5"
"github.com/spf13/cast"
) )
// MultipartJsonKey is the key for the special multipart/form-data // MultipartJsonKey is the key for the special multipart/form-data
@ -144,12 +145,16 @@ func bindFormData(c echo.Context, i any) error {
return echo.BindBody(c, i) return echo.BindBody(c, i)
} }
var inferNumberCharsRegex = regexp.MustCompile(`^[\-\.\d]+$`)
// In order to support more seamlessly both json and multipart/form-data requests, // In order to support more seamlessly both json and multipart/form-data requests,
// the following normalization rules are applied for plain multipart string values: // the following normalization rules are applied for plain multipart string values:
// - "true" is converted to the json `true` // - "true" is converted to the json "true"
// - "false" is converted to the json `false` // - "false" is converted to the json "false"
// - numeric (non-scientific) strings are converted to json number // - numeric strings are converted to json number ONLY if the resulted
// - any other string (empty string too) is left as it is // minimal number string representation is the same as the provided raw string
// (aka. scientific notations, "Infinity", "0.0", "0001", etc. are kept as string)
// - any other string (empty string too) is left as it is
func normalizeMultipartValue(raw string) any { func normalizeMultipartValue(raw string) any {
switch raw { switch raw {
case "": case "":
@ -159,8 +164,12 @@ func normalizeMultipartValue(raw string) any {
case "false": case "false":
return false return false
default: default:
if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') { // try to convert to number
if v, err := cast.ToFloat64E(raw); err == nil { //
// note: expects the provided raw string to match exactly with the minimal string representation of the parsed float
if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') && inferNumberCharsRegex.Match([]byte(raw)) {
v, err := strconv.ParseFloat(raw, 64)
if err == nil && strconv.FormatFloat(v, 'f', -1, 64) == raw {
return v return v
} }
} }