diff --git a/pkg/action/action.go b/pkg/action/action.go deleted file mode 100644 index 4233d986f5cf0ecd199f64c42b5f6453e0533fce..0000000000000000000000000000000000000000 --- a/pkg/action/action.go +++ /dev/null @@ -1,40 +0,0 @@ -package action - -import ( - "net/url" - "strings" -) - -// URL структура для хранения данных о переданном действии. -type URL struct { - *url.URL -} - -// NewURL возвращает структуру ActionURL -func NewURL(action string) (*URL, error) { - u, err := url.Parse(action) - if err != nil { - return nil, err - } - return &URL{URL: u}, nil -} - -func (u *URL) actionParts() (string, string) { - if u.URL != nil && u.URL.Scheme == "grpc" { - splitPath := strings.Split(strings.TrimLeft(u.Path, "/"), "/") - if len(splitPath) >= 2 { - return splitPath[0], splitPath[1] - } - } - return "", "" -} - -func (u *URL) Action() string { - _, action := u.actionParts() - return action -} - -func (u *URL) Extension() string { - ext, _ := u.actionParts() - return ext -} diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go index 9c788e71e108e85b483ec71a8700b7fc3bb046be..93d6ee612968ac0a7e1c963a05cda4955532f110 100644 --- a/pkg/expr/expr.go +++ b/pkg/expr/expr.go @@ -1,12 +1,21 @@ package expr import ( + "regexp" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/data" compiler2 "github.com/antonmedv/expr/compiler" "github.com/antonmedv/expr/parser" "github.com/antonmedv/expr/vm" "golang.org/x/net/context" ) +var ( + additionalFunctions = []string{"contains", "startsWith", "endsWith", "and", "or", "in", "not"} + isExpression = regexp.MustCompile(`[()}{<>=|&%*+\-\/\]\[\\]`).MatchString +) + const EnvContextKey = "$context" func Eval(ctx context.Context, input string, env map[string]interface{}) (interface{}, error) { @@ -56,3 +65,17 @@ func EvalKV(ctx context.Context, input string, kv ...interface{}) (interface{}, return Eval(ctx, input, m) } + +func IsExpression(input string) bool { + if isExpression(input) { + return true + } + + for _, s := range strings.Fields(input) { + if data.Contains(s, additionalFunctions) { + return true + } + } + + return false +} diff --git a/pkg/expr/expr_test.go b/pkg/expr/expr_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5eafc368bed934e0bd0805f4818442a75256931c --- /dev/null +++ b/pkg/expr/expr_test.go @@ -0,0 +1,51 @@ +package expr + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestIsExpression(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + eval string + want bool + }{ + {"equal", "i == 3", true}, + {"in array", "i in [1,2,3]", true}, + {"contains", "value contains 'some'", true}, + {"contains with . + () $ {} ^", "value contains 'something with . + () $ {} ^'", true}, + {"startsWith", "value startsWith 'some'", true}, + {"startsWith . + () $ {} ^", "value startsWith '. + () $ {} ^'", true}, + {"endsWith", "value endsWith 'some'", true}, + {"endsWith . + () $ {} ^", "value endsWith '. + () $ {} ^'", true}, + {"icontains", "icontains(value, 'some')", true}, + {"icontains with . + () $ {} ^", "icontains (value, 'something with . + () $ {} ^')", true}, + {"istartsWith", "istartsWith(value, 'Some')", true}, + {"istartsWith . + () $ {} ^ . + () $ {} ^", "istartsWith(value, '. + () $ {} ^')", true}, + {"iendsWith", "iendsWith(value, 'some')", true}, + {"iendsWith . + () $ {} ^", "iendsWith(value,'. + () $ {} ^')", true}, + {"or", "i == 2 || i > 10", true}, + {"search", "search('some') || i > 10", true}, + {"vars:or", "i == a + 2 || i > a + 10", true}, + {"near", "near(a, [55.5, 37.5], 1000)", true}, + {"within", "within(a, 'box', [[54.54, 36.36], [55.55, 37.37]])", true}, + {"time", "d > Time.Date('2021-08-31')", true}, + {"time", fmt.Sprintf("d > Time.Time('%s')", now.Format(time.RFC3339)), true}, + {"in", "In(s, [1,2,3])", true}, + {"in", "In(s, 1)", true}, + {"text search or id", "id", false}, + {"numbers", "3", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsExpression(tt.eval) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/extension/action_handler.go b/pkg/extension/action_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..3079b88582a8c392b948bb5d46ff2bbbb7d91e06 --- /dev/null +++ b/pkg/extension/action_handler.go @@ -0,0 +1,39 @@ +package extension + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +var ( + ErrInvalidAction = errors.New("invalid action") +) + +// ActionHandler выполняет действие в url +type ActionHandler func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) + +// ActionRouter возвращает ActionHandler для указанного действия +type ActionRouter func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error) + +// Chain возвращает ActionRouter, который последовательно вызывает переданные ActionRouter +func Chain(chain ...ActionRouter) ActionRouter { + return func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error) { + for _, f := range chain { + if h, err := f(ctx, url, req); err == nil && h != nil { + return h, nil + } + } + return nil, ErrInvalidAction + } +} + +// Named возвращает ActionRouter, который возвращает ActionHandler для указанного действия по имени +func Named(name string, handler ActionHandler) ActionRouter { + return func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error) { + if url.Action() == name { + return handler, nil + } + return nil, ErrInvalidAction + } +} diff --git a/pkg/extension/action_handler_test.go b/pkg/extension/action_handler_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c73a3b87c5e4a372e7a67858ab508cbe7ec93cfc --- /dev/null +++ b/pkg/extension/action_handler_test.go @@ -0,0 +1,99 @@ +package extension + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestActionHandler(t *testing.T) { + tests := []struct { + name string + request *ActionRequest + router ActionRouter + want string + wantErr bool + }{ + { + name: "No named action call", + request: &ActionRequest{Action: "grpc:///ext/action/"}, + router: func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error) { + return func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called - action"}, nil + }, nil + }, + want: "Called - action", + wantErr: false, + }, + { + name: "Named action call", + request: &ActionRequest{Action: "grpc:///ext/action/"}, + router: Named("action", func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called named action"}, nil + }), + want: "Called named action", + wantErr: false, + }, + { + name: "Named action chain call", + request: &ActionRequest{Action: "grpc:///ext/action1/"}, + router: Chain( + Named("action1", func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called named action1"}, nil + }), + Named("action2", func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called named action2"}, nil + }), + ), + want: "Called named action1", + wantErr: false, + }, + { + name: "Error (no action in router)", + request: &ActionRequest{Action: "grpc:///ext/action3/"}, + router: Chain( + Named("action1", func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called named action1"}, nil + }), + Named("action2", func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called named action2"}, nil + }), + ), + wantErr: true, + }, + { + name: "Named action call with deprecated request", + request: &ActionRequest{Action: "action"}, + router: Named("action", func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error) { + return &ActionResponse{State: 0, Msg: "Called named deprecated action"}, nil + }), + want: "Called named deprecated action", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + url, err := NewActionURL(tt.request.Action) + require.NoError(t, err) + + handler, err := tt.router(context.Background(), url, tt.request) + if tt.wantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + + resp, err := handler(context.Background(), url, tt.request) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.want, resp.Msg) + }) + } +} diff --git a/pkg/extension/action_url.go b/pkg/extension/action_url.go new file mode 100644 index 0000000000000000000000000000000000000000..ec03375f840bcd3889cba680e5b9087dd89c7f80 --- /dev/null +++ b/pkg/extension/action_url.go @@ -0,0 +1,42 @@ +package extension + +import ( + "net/url" + "strings" +) + +// URL структура для хранения данных о переданном действии. +type ActionURL struct { + *url.URL +} + +// NewURL возвращает структуру ActionURL +func NewActionURL(action string) (*ActionURL, error) { + u, err := url.Parse(action) + if err != nil { + return nil, err + } + return &ActionURL{URL: u}, nil +} + +func (u *ActionURL) actionParts() (string, string) { + splitPath := strings.Split(strings.TrimLeft(u.Path, "/"), "/") + if u.URL != nil && u.URL.Scheme == "grpc" && len(splitPath) >= 2 { + return splitPath[0], splitPath[1] + } + // for backwards compatibility + if len(splitPath) == 1 { + return "", splitPath[0] + } + return "", "" +} + +func (u *ActionURL) Action() string { + _, action := u.actionParts() + return action +} + +func (u *ActionURL) Extension() string { + ext, _ := u.actionParts() + return ext +} diff --git a/pkg/action/action_test.go b/pkg/extension/action_url_test.go similarity index 71% rename from pkg/action/action_test.go rename to pkg/extension/action_url_test.go index 2988744f4b88b734c4085b5aa20ceb95fe0657da..e4545b39a21922222ff7a0f140f8488007168f22 100644 --- a/pkg/action/action_test.go +++ b/pkg/extension/action_url_test.go @@ -1,4 +1,4 @@ -package action +package extension import ( "fmt" @@ -12,13 +12,13 @@ func TestActionURL_New(t *testing.T) { tests := []struct { name string action string - want *URL + want *ActionURL url string wantErr assert.ErrorAssertionFunc }{ { name: "Without action", - want: &URL{ + want: &ActionURL{ URL: &url.URL{}, }, wantErr: assert.NoError, @@ -26,7 +26,7 @@ func TestActionURL_New(t *testing.T) { { name: "Without deprecated action call", action: "build-site", - want: &URL{ + want: &ActionURL{ URL: &url.URL{ Path: "build-site", }, @@ -37,7 +37,7 @@ func TestActionURL_New(t *testing.T) { { name: "With grpc action", action: "grpc:///perxisweb/build-site", - want: &URL{ + want: &ActionURL{ URL: &url.URL{ Scheme: "grpc", Path: "/perxisweb/build-site", @@ -49,7 +49,7 @@ func TestActionURL_New(t *testing.T) { { name: "With ui action", action: "ui:///space/env/coll", - want: &URL{ + want: &ActionURL{ URL: &url.URL{ Scheme: "ui", Path: "/space/env/coll", @@ -61,7 +61,7 @@ func TestActionURL_New(t *testing.T) { { name: "With http action", action: "https://perx.ru", - want: &URL{ + want: &ActionURL{ URL: &url.URL{ Scheme: "https", Host: "perx.ru", @@ -79,7 +79,7 @@ func TestActionURL_New(t *testing.T) { { name: "With no action id", action: "grpc:///perxisweb", - want: &URL{ + want: &ActionURL{ URL: &url.URL{ Scheme: "grpc", Path: "/perxisweb", @@ -90,7 +90,7 @@ func TestActionURL_New(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewURL(tt.action) + got, err := NewActionURL(tt.action) if !tt.wantErr(t, err, fmt.Sprintf("NewURL(%v)", tt.action)) { return } @@ -128,8 +128,45 @@ func TestActionURL_String(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p, _ := NewURL(tt.url) + p, _ := NewActionURL(tt.url) assert.Equalf(t, tt.url, p.String(), "String()") }) } } + +func TestActionURL_Action(t *testing.T) { + tests := []struct { + name string + url string + want string + }{ + { + name: "GRPC action", + url: "grpc:///perxisweb/build-site", + want: "build-site", + }, + { + name: "UI action #1", + url: "ui:///space/env/coll", + }, + { + name: "UI action deprecated call #2", + url: "space/env/coll", + }, + { + name: "Https action", + url: "https://perx.ru", + }, + { + name: "With action deprecated call", + url: "extension-id", + want: "extension-id", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, _ := NewActionURL(tt.url) + assert.Equalf(t, tt.want, p.Action(), "Action()") + }) + } +} diff --git a/pkg/extension/server.go b/pkg/extension/server.go index 42c1f241e93bca687c5cf0afd75d63a74ceb22f2..0b16125450a89d001ab6fa2228454ad03085b6bb 100644 --- a/pkg/extension/server.go +++ b/pkg/extension/server.go @@ -4,7 +4,6 @@ import ( "context" "strings" - "git.perx.ru/perxis/perxis-go/pkg/action" "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/operation" commonpb "git.perx.ru/perxis/perxis-go/proto/common" @@ -162,14 +161,15 @@ func (s *Server) Check(ctx context.Context, req *CheckRequest) (*operation.Proto func (s *Server) Action(ctx context.Context, in *pb.ActionRequest) (*pb.ActionResponse, error) { actionURL, err := action.NewURL(in.Action) if err != nil { - return nil, err + return nil, ErrInvalidAction } + ext := actionURL.Extension() if ext == "" { ext = in.Extension } if ext == "" { - return nil, errors.New("extension ID required") + return nil, ErrInvalidAction } svc, err := s.extensions(ctx, ext) diff --git a/pkg/extension/server_test.go b/pkg/extension/server_test.go index f80988fab0c7846ed79bcf340cbd22a0748b13c5..8c93f48d00d87c45363a04eec2fbc55153cbc45b 100644 --- a/pkg/extension/server_test.go +++ b/pkg/extension/server_test.go @@ -69,7 +69,7 @@ func TestServer_Action(t *testing.T) { EnvId: "env", }, want: &ActionResponse{State: ResponseDone}, - wantErr: "extension ID required", + wantErr: "invalid action", }, { name: "Deprecated call", @@ -91,7 +91,7 @@ func TestServer_Action(t *testing.T) { EnvId: "env", }, want: nil, - wantErr: errors.Wrap(ErrUnknownExtension, "test-extension-2").Error(), + wantErr: "invalid action", }, { name: "Deprecated call, without extension", @@ -102,7 +102,7 @@ func TestServer_Action(t *testing.T) { EnvId: "env", }, want: nil, - wantErr: "extension ID required", + wantErr: "invalid action", }, { name: "Deprecated call, without action and extension)", @@ -112,7 +112,7 @@ func TestServer_Action(t *testing.T) { EnvId: "env", }, want: nil, - wantErr: "extension ID required", + wantErr: "invalid action", }, } for _, tt := range tests { diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go index 370e56529f1581db74f7dd9a2df4b7ba0b5138bf..995c343467fef3a9b1963ef5d65a1c230ad4c69d 100644 --- a/pkg/extension/service/extension.go +++ b/pkg/extension/service/extension.go @@ -4,10 +4,8 @@ import ( "context" "fmt" - "git.perx.ru/perxis/perxis-go/pkg/action" "git.perx.ru/perxis/perxis-go/pkg/clients" "git.perx.ru/perxis/perxis-go/pkg/content" - "git.perx.ru/perxis/perxis-go/pkg/errors" "git.perx.ru/perxis/perxis-go/pkg/extension" "git.perx.ru/perxis/perxis-go/pkg/roles" "git.perx.ru/perxis/perxis-go/pkg/setup" @@ -32,6 +30,7 @@ type Extension struct { Content *content.Content Logger *zap.Logger manager extension.Manager + router extension.ActionRouter withClient bool role *roles.Role @@ -39,12 +38,13 @@ type Extension struct { keyFn extension.KeyFunc } -func NewExtension(desc *extension.ExtensionDescriptor, cnt *content.Content, setupFunc SetupFunc, logger *zap.Logger) *Extension { +func NewExtension(desc *extension.ExtensionDescriptor, cnt *content.Content, setupFunc SetupFunc, router extension.ActionRouter, logger *zap.Logger) *Extension { if logger == nil { logger = zap.NewNop() } return &Extension{ desc: desc, + router: router, setupFunc: setupFunc, Content: cnt, Logger: logger, @@ -151,25 +151,36 @@ func (s *Extension) Uninstall(ctx context.Context, in *extension.UninstallReques return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).WithRemove(in.Remove).Uninstall(ctx) } -func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) { - actionURL, err := action.NewURL(in.Action) - if err != nil { - return nil, err - } - ext := actionURL.Extension() +// isCorrectExtension проверяет что расширение в url совпадает с расширением расширения +func (s *Extension) isCorrectExtension(ctx context.Context, url *extension.ActionURL, in *extension.ActionRequest) bool { + ext := url.Extension() if ext == "" { ext = in.Extension } - if ext == "" { - return nil, errors.New("extension ID required") - } - ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, ext) - if err != nil { - return nil, errors.Wrap(err, "check extension installed") + if ext == s.desc.Extension { + return true } - if !ok { - return nil, errors.New("extension not installed") + return false +} + +func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) { + // TBD: нужно ли проверять что действие установлено в пространство + // мы так и не договорились, но проверка появилась + // пусть решает каждое расширение само + // + //ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, ext) + //if err != nil { + // return nil, errors.Wrap(err, "check extension installed") + //} + //if !ok { + // return nil, errors.New("extension not installed") + //} + + if url, err := extension.NewActionURL(in.Action); s.router != nil && err == nil && url != nil && s.isCorrectExtension(ctx, url, in) { + if h, err := s.router(ctx, url, in); err == nil && h != nil { + return h(ctx, url, in) + } } - return &extension.ActionResponse{}, nil + return nil, extension.ErrInvalidAction } diff --git a/pkg/extension/service/extension_test.go b/pkg/extension/service/extension_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e0582966ae25186f6ddef5014fdd972544521f8a --- /dev/null +++ b/pkg/extension/service/extension_test.go @@ -0,0 +1,68 @@ +package service + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/extension" + "github.com/stretchr/testify/require" +) + +func TestExtension_Action(t *testing.T) { + tests := []struct { + name string + desc *extension.ExtensionDescriptor + in *extension.ActionRequest + router extension.ActionRouter + want string + wantErr bool + }{ + { + name: "Router with 1 named action", + desc: &extension.ExtensionDescriptor{Extension: "test", Title: "Test Extension", Description: "desc", Version: "v.0.0.1"}, + in: &extension.ActionRequest{Action: "grpc:///test/action", SpaceId: "sp", EnvId: "env", CollectionId: "coll"}, + want: "Called action", + router: extension.Named("action", func(ctx context.Context, url *extension.ActionURL, req *extension.ActionRequest) (*extension.ActionResponse, error) { + return &extension.ActionResponse{State: extension.ResponseDone, Msg: "Called action"}, nil + }), + wantErr: false, + }, + { + name: "Router with 2 named actions", + desc: &extension.ExtensionDescriptor{Extension: "test", Title: "Test Extension", Description: "desc", Version: "v.0.0.1"}, + in: &extension.ActionRequest{Action: "grpc:///test/action2", SpaceId: "sp", EnvId: "env", CollectionId: "coll"}, + want: "Called action2", + router: extension.Chain(extension.Named("action", func(ctx context.Context, url *extension.ActionURL, req *extension.ActionRequest) (*extension.ActionResponse, error) { + return &extension.ActionResponse{State: extension.ResponseDone}, nil + }), extension.Named("action2", func(ctx context.Context, url *extension.ActionURL, req *extension.ActionRequest) (*extension.ActionResponse, error) { + return &extension.ActionResponse{State: extension.ResponseDone, Msg: "Called action2"}, nil + })), + wantErr: false, + }, + { + name: "Error (no extension)", + desc: &extension.ExtensionDescriptor{Extension: "test", Title: "Test Extension", Description: "desc", Version: "v.0.0.1"}, + in: &extension.ActionRequest{Action: "grpc:///test2/action2", SpaceId: "sp", EnvId: "env", CollectionId: "coll"}, + router: extension.Chain(extension.Named("action", func(ctx context.Context, url *extension.ActionURL, req *extension.ActionRequest) (*extension.ActionResponse, error) { + return &extension.ActionResponse{State: extension.ResponseDone}, nil + }), extension.Named("action2", func(ctx context.Context, url *extension.ActionURL, req *extension.ActionRequest) (*extension.ActionResponse, error) { + return &extension.ActionResponse{State: extension.ResponseParametersRequired}, nil + })), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Extension{ + desc: tt.desc, + router: tt.router, + } + got, err := s.Action(context.Background(), tt.in) + if tt.wantErr { + require.Error(t, err) + return + } + require.Equal(t, tt.want, got.Msg) + }) + } +} diff --git a/pkg/schema/field/number.go b/pkg/schema/field/number.go index 502a2fef7c406b4b4dc96aad34efce2e518a1e28..40d9c8167f54d23faaa40666ec1934b451d7a516 100644 --- a/pkg/schema/field/number.go +++ b/pkg/schema/field/number.go @@ -10,6 +10,10 @@ import ( const ( NumberFormatInt = "int" NumberFormatFloat = "float" + + // The largest integer that can be represented in IEEE 754 double (64-bit) is a value from -2^53 to 2^53. + maxInt = 1<<53 - 1 + minInt = -1<<53 + 1 ) var numberType = &NumberType{} @@ -84,10 +88,19 @@ func (NumberType) decode(_ context.Context, field *Field, v interface{}) (interf case NumberFormatInt: switch i := n.(type) { case int64: + if i > maxInt || i < minInt { + return nil, errors.New("integer out of range") + } return i, nil case uint64: + if i > maxInt { + return nil, errors.New("integer out of range") + } return i, nil case float64: + if i > maxInt || i < minInt { + return nil, errors.New("integer out of range") + } return int64(math.Round(i)), nil } case NumberFormatFloat: diff --git a/pkg/schema/field/number_test.go b/pkg/schema/field/number_test.go index e731793d12135127dbfa67d949572f52708075a6..367ffe4249f36f2dff17aa1e61ef6a753ccabee8 100644 --- a/pkg/schema/field/number_test.go +++ b/pkg/schema/field/number_test.go @@ -1,6 +1,8 @@ package field import ( + "context" + "math" "reflect" "testing" ) @@ -13,18 +15,25 @@ func TestNumberField_Decode(t *testing.T) { want interface{} wantErr bool }{ - {"Correct", Number("int"), int64(2), int64(2), false}, // #0 - {"Correct", Number("int"), 2.2, int64(2), false}, // #1 - {"Correct", Number("int"), 2, int64(2), false}, // #2 - {"Correct", Number("int"), float32(2.2), int64(2), false}, // #3 - {"Correct", Number("int"), float64(2.6), int64(3), false}, // #4 - {"Correct", Number("int"), 2.6, int64(3), false}, // #5 + {"Correct", Number("int"), int64(2), int64(2), false}, // #0 + {"Correct", Number("int"), 2.2, int64(2), false}, // #1 + {"Correct", Number("int"), 2, int64(2), false}, // #2 + {"Correct", Number("int"), float32(2.2), int64(2), false}, // #3 + {"Correct", Number("int"), float64(2.6), int64(3), false}, // #4 + {"Correct", Number("int"), 2.6, int64(3), false}, // #5 + {"MaxInt64", Number(NumberFormatInt), int64(math.MaxInt64), nil, true}, // #6 + {"MinInt64", Number(NumberFormatInt), int64(math.MinInt64), nil, true}, // #7 + {"maxInt in float", Number(NumberFormatInt), float64(maxInt), int64(maxInt), false}, // #8 + {"minInt in float", Number(NumberFormatInt), float64(minInt), int64(minInt), false}, // #9 + {"Convert error", Number(NumberFormatInt), math.MaxFloat64, nil, true}, // #10 + {"Convert error", Number(NumberFormatInt), -math.MaxFloat64, nil, true}, // #11 + {"Convert error", Number(NumberFormatInt), float64(math.MaxInt64), nil, true}, // #13 + {"Convert error", Number(NumberFormatInt), float64(math.MinInt64), nil, true}, // #14 - {"Correct", Number("float"), int8(2), 2.0, false}, // #6 - {"Correct", Number("float"), 2.2, 2.2, false}, // #7 - {"Correct", Number("float"), 2, 2.0, false}, // #8 - {"Correct", Number("float"), float32(2.2), 2.200000047683716, false}, // #9 - {"Correct", Number("float"), int64(2), 2.0, false}, // #10 + {"Correct", Number("float"), int8(2), 2.0, false}, // #15 + {"Correct", Number("float"), 2.2, 2.2, false}, // #16 + {"Correct", Number("float"), 2, 2.0, false}, // #17 + {"Correct", Number("float"), int64(2), 2.0, false}, // #18 {"Wrong data", Number("int"), "", nil, true}, // #0 {"Wrong data", Number("int"), []byte(""), nil, true}, // #1 @@ -34,7 +43,7 @@ func TestNumberField_Decode(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Decode(nil, tt.field, tt.data) + got, err := Decode(context.Background(), tt.field, tt.data) if (err != nil) != tt.wantErr { t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) return @@ -61,11 +70,10 @@ func TestNumberField_Encode(t *testing.T) { {"Correct", Number("int"), float32(2.6), int64(3), false}, // #4 {"Correct", Number("int"), 2.6, int64(3), false}, // #5 - {"Correct", Number("float"), int8(2), 2.0, false}, // #6 - {"Correct", Number("float"), 2.2, 2.2, false}, // #7 - {"Correct", Number("float"), 2, 2.0, false}, // #8 - {"Correct", Number("float"), float32(2.2), 2.200000047683716, false}, // #9 - {"Correct", Number("float"), int64(2), 2.0, false}, // #10 + {"Correct", Number("float"), int8(2), 2.0, false}, // #6 + {"Correct", Number("float"), 2.2, 2.2, false}, // #7 + {"Correct", Number("float"), 2, 2.0, false}, // #8 + {"Correct", Number("float"), int64(2), 2.0, false}, // #9 {"Wrong data", Number("int"), "", nil, true}, // #0 {"Wrong data", Number("int"), []byte(""), nil, true}, // #1 @@ -75,7 +83,7 @@ func TestNumberField_Encode(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Encode(nil, tt.field, tt.data) + got, err := Encode(context.Background(), tt.field, tt.data) if (err != nil) != tt.wantErr { t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/schema/test/object_test.go b/pkg/schema/test/object_test.go index e5af975cdb9ed8e86044e5d357530dd2420afee9..38868f66989108c142e50eaad62539be381edb4f 100644 --- a/pkg/schema/test/object_test.go +++ b/pkg/schema/test/object_test.go @@ -74,6 +74,80 @@ func TestNumberField_JSON(t *testing.T) { assert.Equal(t, fld, res) } +func TestNumberField_JSON_With_MaxInt(t *testing.T) { + t.Run("Without overflow from int64", func(t *testing.T) { + fld := field.Object("num", field.Number(field.NumberFormatInt)) + + itemData := map[string]interface{}{"num": 1<<53 - 1} + + b, err := json.Marshal(itemData) + require.NoError(t, err) + + itemDataToDecode := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(b, &itemDataToDecode)) + + _, err = field.Decode(context.Background(), fld, itemDataToDecode) + require.NoError(t, err) + }) + t.Run("Without overflow from float64", func(t *testing.T) { + fld := field.Object("num", field.Number(field.NumberFormatInt)) + + itemData := map[string]interface{}{"num": float64(1<<53 - 1)} + + b, err := json.Marshal(itemData) + require.NoError(t, err) + + itemDataToDecode := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(b, &itemDataToDecode)) + + _, err = field.Decode(context.Background(), fld, itemDataToDecode) + require.NoError(t, err) + }) + t.Run("With overflow from int64", func(t *testing.T) { + fld := field.Object("num", field.Number(field.NumberFormatInt)) + + itemData := map[string]interface{}{"num": 1 << 53} + + b, err := json.Marshal(itemData) + require.NoError(t, err) + + itemDataToDecode := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(b, &itemDataToDecode)) + + _, err = field.Decode(context.Background(), fld, itemDataToDecode) + require.Error(t, err, "integer out of range") + }) + t.Run("With overflow from uint64", func(t *testing.T) { + fld := field.Object("num", field.Number(field.NumberFormatInt)) + + itemData := map[string]interface{}{"num": uint64(1 << 53)} + + b, err := json.Marshal(itemData) + require.NoError(t, err) + + itemDataToDecode := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(b, &itemDataToDecode)) + + _, err = field.Decode(context.Background(), fld, itemDataToDecode) + require.Error(t, err, "integer out of range") + }) + t.Run("With overflow from float64", func(t *testing.T) { + fld := field.Object("num", field.Number(field.NumberFormatInt)) + + itemData := map[string]interface{}{"num": float64(1 << 53)} + + b, err := json.Marshal(itemData) + require.NoError(t, err) + + itemDataToDecode := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(b, &itemDataToDecode)) + + _, err = field.Decode(context.Background(), fld, itemDataToDecode) + require.Error(t, err, "integer out of range") + }) + +} + func TestSchema_JSON(t *testing.T) { enumStr := field.String().AddOptions( validate.Enum(