Compare commits
18 Commits
32aff3e07f
...
3606f9984d
Author | SHA1 | Date |
---|---|---|
Adrien Marquès | 3606f9984d | |
Adrien Marquès | 7b812c6648 | |
Adrien Marquès | dc34d9a81a | |
Adrien Marquès | cdbe4cceac | |
Adrien Marquès | 03d5e87c37 | |
Adrien Marquès | c7aa87c660 | |
Adrien Marquès | 0f62fc25a0 | |
Adrien Marquès | 8c539370aa | |
Adrien Marquès | acd0e73438 | |
Adrien Marquès | b38a9a8111 | |
Adrien Marquès | 93b31b9718 | |
Adrien Marquès | 12417f7f1c | |
Adrien Marquès | e7f10723a6 | |
Adrien Marquès | c32b038da2 | |
Adrien Marquès | 1b4922693b | |
Adrien Marquès | 4e0d669029 | |
Adrien Marquès | 2c1b9cf5ff | |
Adrien Marquès | d1ab4fefb0 |
|
@ -2,15 +2,21 @@ package api
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.xdrm.io/go/aicra/internal/cerr"
|
||||
)
|
||||
|
||||
// Error allows you to create constant "const" error with type boxing.
|
||||
type Error string
|
||||
|
||||
// Error implements the error builtin interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// ErrReqParamNotFound is thrown when a request parameter is not found
|
||||
const ErrReqParamNotFound = cerr.Error("request parameter not found")
|
||||
const ErrReqParamNotFound = Error("request parameter not found")
|
||||
|
||||
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
|
||||
const ErrReqParamNotType = cerr.Error("request parameter does not fulfills type")
|
||||
const ErrReqParamNotType = Error("request parameter does not fulfills type")
|
||||
|
||||
// RequestParam defines input parameters of an api request
|
||||
type RequestParam map[string]interface{}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package builtin
|
||||
|
||||
import "git.xdrm.io/go/aicra/config/datatype"
|
||||
import "git.xdrm.io/go/aicra/datatype"
|
||||
|
||||
// AnyDataType is what its name tells
|
||||
type AnyDataType struct{}
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestAny_AvailableTypes(t *testing.T) {
|
|
@ -1,6 +1,6 @@
|
|||
package builtin
|
||||
|
||||
import "git.xdrm.io/go/aicra/config/datatype"
|
||||
import "git.xdrm.io/go/aicra/datatype"
|
||||
|
||||
// BoolDataType is what its name tells
|
||||
type BoolDataType struct{}
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestBool_AvailableTypes(t *testing.T) {
|
|
@ -3,7 +3,7 @@ package builtin
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
// FloatDataType is what its name tells
|
|
@ -5,7 +5,7 @@ import (
|
|||
"math"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestFloat64_AvailableTypes(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"math"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
// IntDataType is what its name tells
|
|
@ -5,7 +5,7 @@ import (
|
|||
"math"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestInt_AvailableTypes(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
|
@ -4,7 +4,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestString_AvailableTypes(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"math"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
// UintDataType is what its name tells
|
|
@ -5,7 +5,7 @@ import (
|
|||
"math"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestUint_AvailableTypes(t *testing.T) {
|
|
@ -4,9 +4,9 @@ package datatype
|
|||
// and casts the value into a compatible type
|
||||
type Validator func(value interface{}) (cast interface{}, valid bool)
|
||||
|
||||
// DataType builds a DataType from the type definition (from the
|
||||
// T builds a T from the type definition (from the
|
||||
// configuration field "type") and returns NIL if the type
|
||||
// definition does not match this DataType
|
||||
type DataType interface {
|
||||
// definition does not match this T
|
||||
type T interface {
|
||||
Build(typeDefinition string) Validator
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package cerr
|
||||
|
||||
// Error allows you to create constant "const" error with type boxing.
|
||||
type Error string
|
||||
|
||||
// Error implements the error builtin interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// Wrap returns a new error which wraps a new error into itself.
|
||||
func (err Error) Wrap(e error) *WrapError {
|
||||
return &WrapError{
|
||||
base: err,
|
||||
wrap: e,
|
||||
}
|
||||
}
|
||||
|
||||
// WrapString returns a new error which wraps a new error created from a string.
|
||||
func (err Error) WrapString(e string) *WrapError {
|
||||
return &WrapError{
|
||||
base: err,
|
||||
wrap: Error(e),
|
||||
}
|
||||
}
|
||||
|
||||
// WrapError is way to wrap errors recursively.
|
||||
type WrapError struct {
|
||||
base error
|
||||
wrap error
|
||||
}
|
||||
|
||||
// Error implements the error builtin interface recursively.
|
||||
func (err *WrapError) Error() string {
|
||||
return err.base.Error() + ": " + err.wrap.Error()
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package cerr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConstError(t *testing.T) {
|
||||
const cerr1 = Error("some-string")
|
||||
const cerr2 = Error("some-other-string")
|
||||
const cerr3 = Error("some-string") // same const value as @cerr1
|
||||
|
||||
if cerr1.Error() == cerr2.Error() {
|
||||
t.Errorf("cerr1 should not be equal to cerr2 ('%s', '%s')", cerr1.Error(), cerr2.Error())
|
||||
}
|
||||
if cerr2.Error() == cerr3.Error() {
|
||||
t.Errorf("cerr2 should not be equal to cerr3 ('%s', '%s')", cerr2.Error(), cerr3.Error())
|
||||
}
|
||||
if cerr1.Error() != cerr3.Error() {
|
||||
t.Errorf("cerr1 should be equal to cerr3 ('%s', '%s')", cerr1.Error(), cerr3.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedConstError(t *testing.T) {
|
||||
const parent = Error("file error")
|
||||
|
||||
const readErrorConst = Error("cannot read file")
|
||||
var wrappedReadError = parent.Wrap(readErrorConst)
|
||||
|
||||
expectedWrappedReadError := fmt.Sprintf("%s: %s", parent.Error(), readErrorConst.Error())
|
||||
if wrappedReadError.Error() != expectedWrappedReadError {
|
||||
t.Errorf("expected '%s' (got '%s')", wrappedReadError.Error(), expectedWrappedReadError)
|
||||
}
|
||||
}
|
||||
func TestWrappedStandardError(t *testing.T) {
|
||||
const parent = Error("file error")
|
||||
|
||||
var writeErrorStandard error = errors.New("cannot write file")
|
||||
var wrappedWriteError = parent.Wrap(writeErrorStandard)
|
||||
|
||||
expectedWrappedWriteError := fmt.Sprintf("%s: %s", parent.Error(), writeErrorStandard.Error())
|
||||
if wrappedWriteError.Error() != expectedWrappedWriteError {
|
||||
t.Errorf("expected '%s' (got '%s')", wrappedWriteError.Error(), expectedWrappedWriteError)
|
||||
}
|
||||
}
|
||||
func TestWrappedStringError(t *testing.T) {
|
||||
const parent = Error("file error")
|
||||
|
||||
var closeErrorString string = "cannot close file"
|
||||
var wrappedCloseError = parent.WrapString(closeErrorString)
|
||||
|
||||
expectedWrappedCloseError := fmt.Sprintf("%s: %s", parent.Error(), closeErrorString)
|
||||
if wrappedCloseError.Error() != expectedWrappedCloseError {
|
||||
t.Errorf("expected '%s' (got '%s')", wrappedCloseError.Error(), expectedWrappedCloseError)
|
||||
}
|
||||
}
|
|
@ -8,10 +8,12 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||
)
|
||||
|
||||
func TestLegalServiceName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
Raw string
|
||||
Error error
|
||||
|
@ -43,35 +45,35 @@ func TestLegalServiceName(t *testing.T) {
|
|||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
||||
ErrInvalidPatternBracePosition,
|
||||
ErrInvalidPatternBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
||||
ErrInvalidPatternBracePosition,
|
||||
ErrInvalidPatternBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
||||
nil,
|
||||
ErrUndefinedBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
||||
ErrInvalidPatternBracePosition,
|
||||
ErrInvalidPatternBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
||||
ErrInvalidPatternBracePosition,
|
||||
ErrInvalidPatternBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
||||
nil,
|
||||
ErrUndefinedBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
||||
ErrInvalidPatternOpeningBrace,
|
||||
ErrInvalidPatternBraceCapture,
|
||||
},
|
||||
{
|
||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
||||
ErrInvalidPatternClosingBrace,
|
||||
ErrInvalidPatternBraceCapture,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -99,6 +101,7 @@ func TestLegalServiceName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
func TestAvailableMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
Raw string
|
||||
ValidMethod bool
|
||||
|
@ -146,6 +149,7 @@ func TestAvailableMethods(t *testing.T) {
|
|||
}
|
||||
}
|
||||
func TestParseEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
reader := strings.NewReader(`[]`)
|
||||
_, err := Parse(reader)
|
||||
if err != nil {
|
||||
|
@ -167,6 +171,7 @@ func TestParseJsonError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseMissingMethodDescription(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
Raw string
|
||||
ValidDescription bool
|
||||
|
@ -217,6 +222,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||
t.Parallel()
|
||||
reader := strings.NewReader(`[
|
||||
{
|
||||
"method": "GET",
|
||||
|
@ -233,12 +239,12 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
if len(srv.services) < 1 {
|
||||
if len(srv.Services) < 1 {
|
||||
t.Errorf("expected a service")
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
for _, param := range srv.services[0].Input {
|
||||
for _, param := range srv.Services[0].Input {
|
||||
if param.Rename != "original" {
|
||||
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
|
||||
t.FailNow()
|
||||
|
@ -247,6 +253,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
|||
|
||||
}
|
||||
func TestOptionalParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
reader := strings.NewReader(`[
|
||||
{
|
||||
"method": "GET",
|
||||
|
@ -266,11 +273,11 @@ func TestOptionalParam(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
if len(srv.services) < 1 {
|
||||
if len(srv.Services) < 1 {
|
||||
t.Errorf("expected a service")
|
||||
t.FailNow()
|
||||
}
|
||||
for pName, param := range srv.services[0].Input {
|
||||
for pName, param := range srv.Services[0].Input {
|
||||
|
||||
if pName == "optional" || pName == "optional2" {
|
||||
if !param.Optional {
|
||||
|
@ -288,6 +295,7 @@ func TestOptionalParam(t *testing.T) {
|
|||
|
||||
}
|
||||
func TestParseParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
Raw string
|
||||
Error error
|
||||
|
@ -303,7 +311,7 @@ func TestParseParameters(t *testing.T) {
|
|||
}
|
||||
}
|
||||
]`,
|
||||
ErrIllegalParamName,
|
||||
ErrMissingParamDesc,
|
||||
},
|
||||
{ // invalid param name suffix
|
||||
`[
|
||||
|
@ -316,7 +324,7 @@ func TestParseParameters(t *testing.T) {
|
|||
}
|
||||
}
|
||||
]`,
|
||||
ErrIllegalParamName,
|
||||
ErrMissingParamDesc,
|
||||
},
|
||||
|
||||
{ // missing param description
|
||||
|
@ -473,6 +481,57 @@ func TestParseParameters(t *testing.T) {
|
|||
]`,
|
||||
nil,
|
||||
},
|
||||
|
||||
{ // URI parameter
|
||||
`[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/{uri}",
|
||||
"info": "info",
|
||||
"in": {
|
||||
"{uri}": { "info": "valid", "type": "any", "name": "freename" }
|
||||
}
|
||||
}
|
||||
]`,
|
||||
nil,
|
||||
},
|
||||
{ // URI parameter cannot be optional
|
||||
`[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/{uri}",
|
||||
"info": "info",
|
||||
"in": {
|
||||
"{uri}": { "info": "valid", "type": "?any", "name": "freename" }
|
||||
}
|
||||
}
|
||||
]`,
|
||||
ErrIllegalOptionalURIParam,
|
||||
},
|
||||
{ // URI parameter not specified
|
||||
`[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"info": "info",
|
||||
"in": {
|
||||
"{uri}": { "info": "valid", "type": "?any", "name": "freename" }
|
||||
}
|
||||
}
|
||||
]`,
|
||||
ErrUnspecifiedBraceCapture,
|
||||
},
|
||||
{ // URI parameter not defined
|
||||
`[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/{uri}",
|
||||
"info": "info",
|
||||
"in": { }
|
||||
}
|
||||
]`,
|
||||
ErrUndefinedBraceCapture,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
@ -500,8 +559,8 @@ func TestParseParameters(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
// todo: rewrite with new api format
|
||||
func TestMatchSimple(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
Config string
|
||||
URL string
|
||||
|
@ -583,7 +642,7 @@ func TestMatchSimple(t *testing.T) {
|
|||
"path": "/a/{valid}",
|
||||
"info": "info",
|
||||
"in": {
|
||||
"{id}": {
|
||||
"{valid}": {
|
||||
"info": "info",
|
||||
"type": "bool"
|
||||
}
|
||||
|
@ -598,7 +657,7 @@ func TestMatchSimple(t *testing.T) {
|
|||
"path": "/a/{valid}",
|
||||
"info": "info",
|
||||
"in": {
|
||||
"{id}": {
|
||||
"{valid}": {
|
||||
"info": "info",
|
||||
"type": "bool"
|
||||
}
|
||||
|
@ -619,14 +678,14 @@ func TestMatchSimple(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
if len(srv.services) != 1 {
|
||||
t.Errorf("expected to have 1 service, got %d", len(srv.services))
|
||||
if len(srv.Services) != 1 {
|
||||
t.Errorf("expected to have 1 service, got %d", len(srv.Services))
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, test.URL, nil)
|
||||
|
||||
match := srv.services[0].Match(req)
|
||||
match := srv.Services[0].Match(req)
|
||||
if test.Match && !match {
|
||||
t.Errorf("expected '%s' to match", test.URL)
|
||||
t.FailNow()
|
|
@ -23,18 +23,21 @@ const ErrPatternCollision = Error("invalid config format")
|
|||
// ErrInvalidPattern - a service pattern is malformed
|
||||
const ErrInvalidPattern = Error("must begin with a '/' and not end with")
|
||||
|
||||
// ErrInvalidPatternBracePosition - a service pattern opening/closing brace is not directly between '/'
|
||||
const ErrInvalidPatternBracePosition = Error("capturing braces must be alone between slashes")
|
||||
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
|
||||
const ErrInvalidPatternBraceCapture = Error("invalid uri capturing braces")
|
||||
|
||||
// ErrInvalidPatternOpeningBrace - a service pattern opening brace is invalid
|
||||
const ErrInvalidPatternOpeningBrace = Error("opening brace already open")
|
||||
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
|
||||
const ErrUnspecifiedBraceCapture = Error("capturing brace missing in the path")
|
||||
|
||||
// ErrInvalidPatternClosingBrace - a service pattern closing brace is invalid
|
||||
const ErrInvalidPatternClosingBrace = Error("closing brace already closed")
|
||||
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
|
||||
const ErrUndefinedBraceCapture = Error("capturing brace missing input definition")
|
||||
|
||||
// ErrMissingDescription - a service is missing its description
|
||||
const ErrMissingDescription = Error("missing description")
|
||||
|
||||
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional
|
||||
const ErrIllegalOptionalURIParam = Error("URI parameter cannot be optional")
|
||||
|
||||
// ErrMissingParamDesc - a parameter is missing its description
|
||||
const ErrMissingParamDesc = Error("missing parameter description")
|
||||
|
||||
|
@ -42,7 +45,7 @@ const ErrMissingParamDesc = Error("missing parameter description")
|
|||
const ErrUnknownDataType = Error("unknown data type")
|
||||
|
||||
// ErrIllegalParamName - a parameter has an illegal name
|
||||
const ErrIllegalParamName = Error("parameter name must not begin/end with '_'")
|
||||
const ErrIllegalParamName = Error("illegal parameter name")
|
||||
|
||||
// ErrMissingParamType - a parameter has an illegal type
|
||||
const ErrMissingParamType = Error("missing parameter type")
|
|
@ -2,8 +2,8 @@ package config
|
|||
|
||||
import "strings"
|
||||
|
||||
// splits an URL without empty sets
|
||||
func splitURL(url string) []string {
|
||||
// SplitURL without empty sets
|
||||
func SplitURL(url string) []string {
|
||||
trimmed := strings.Trim(url, " /\t\r\n")
|
||||
split := strings.Split(trimmed, "/")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package config
|
||||
|
||||
import "git.xdrm.io/go/aicra/config/datatype"
|
||||
import "git.xdrm.io/go/aicra/datatype"
|
||||
|
||||
func (param *Parameter) checkAndFormat() error {
|
||||
|
||||
|
@ -24,7 +24,7 @@ func (param *Parameter) checkAndFormat() error {
|
|||
}
|
||||
|
||||
// assigns the first matching data type from the type definition
|
||||
func (param *Parameter) assignDataType(types []datatype.DataType) bool {
|
||||
func (param *Parameter) assignDataType(types []datatype.T) bool {
|
||||
for _, dtype := range types {
|
||||
param.Validator = dtype.Build(param.Type)
|
||||
if param.Validator != nil {
|
|
@ -7,23 +7,23 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
// Parse builds a server configuration from a json reader and checks for most format errors.
|
||||
// you can provide additional DataTypes as variadic arguments
|
||||
func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) {
|
||||
func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) {
|
||||
server := &Server{
|
||||
types: make([]datatype.DataType, 0),
|
||||
services: make([]*Service, 0),
|
||||
Types: make([]datatype.T, 0),
|
||||
Services: make([]*Service, 0),
|
||||
}
|
||||
// add data types
|
||||
for _, dtype := range dtypes {
|
||||
server.types = append(server.types, dtype)
|
||||
server.Types = append(server.Types, dtype)
|
||||
}
|
||||
|
||||
// parse JSON
|
||||
if err := json.NewDecoder(r).Decode(&server.services); err != nil {
|
||||
if err := json.NewDecoder(r).Decode(&server.Services); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
||||
}
|
||||
|
||||
|
@ -40,23 +40,34 @@ func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) {
|
|||
return server, nil
|
||||
}
|
||||
|
||||
// Find a service matching an incoming HTTP request
|
||||
func (server Server) Find(r *http.Request) *Service {
|
||||
for _, service := range server.Services {
|
||||
if matches := service.Match(r); matches {
|
||||
return service
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collide returns if there is collision between services
|
||||
func (server *Server) collide() error {
|
||||
length := len(server.services)
|
||||
length := len(server.Services)
|
||||
|
||||
// for each service combination
|
||||
for a := 0; a < length; a++ {
|
||||
for b := a + 1; b < length; b++ {
|
||||
aService := server.services[a]
|
||||
bService := server.services[b]
|
||||
aService := server.Services[a]
|
||||
bService := server.Services[b]
|
||||
|
||||
// ignore different method
|
||||
if aService.Method != bService.Method {
|
||||
continue
|
||||
}
|
||||
|
||||
aParts := splitURL(aService.Pattern)
|
||||
bParts := splitURL(bService.Pattern)
|
||||
aParts := SplitURL(aService.Pattern)
|
||||
bParts := SplitURL(bService.Pattern)
|
||||
|
||||
// not same size
|
||||
if len(aParts) != len(bParts) {
|
||||
|
@ -120,20 +131,9 @@ func (server *Server) collide() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Find a service matching an incoming HTTP request
|
||||
func (server Server) Find(r *http.Request) *Service {
|
||||
for _, service := range server.services {
|
||||
if service.Match(r) {
|
||||
return service
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
||||
func (server Server) checkAndFormat() error {
|
||||
for _, service := range server.services {
|
||||
for _, service := range server.Services {
|
||||
|
||||
// check method
|
||||
err := service.checkMethod()
|
||||
|
@ -154,11 +154,18 @@ func (server Server) checkAndFormat() error {
|
|||
}
|
||||
|
||||
// check input parameters
|
||||
err = service.checkAndFormatInput(server.types)
|
||||
err = service.checkAndFormatInput(server.Types)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
|
||||
}
|
||||
|
||||
// fail if a brace capture remains undefined
|
||||
for _, capture := range service.Captures {
|
||||
if capture.Ref == nil {
|
||||
return fmt.Errorf("%s '%s' [in]: %s: %w", service.Method, service.Pattern, capture.Name, ErrUndefinedBraceCapture)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -3,11 +3,15 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||
|
||||
// Match returns if this service would handle this HTTP request
|
||||
func (svc *Service) Match(req *http.Request) bool {
|
||||
// method
|
||||
|
@ -21,7 +25,7 @@ func (svc *Service) Match(req *http.Request) bool {
|
|||
}
|
||||
|
||||
// check and extract input
|
||||
// todo: check if input match
|
||||
// todo: check if input match and extract models
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -50,40 +54,40 @@ func (svc *Service) checkPattern() error {
|
|||
}
|
||||
}
|
||||
|
||||
// check capturing braces
|
||||
depth := 0
|
||||
for c, l := 1, length; c < l; c++ {
|
||||
char := svc.Pattern[c]
|
||||
|
||||
if char == '{' {
|
||||
// opening brace when already opened
|
||||
if depth != 0 {
|
||||
return ErrInvalidPatternOpeningBrace
|
||||
// for each slash-separated chunk
|
||||
parts := SplitURL(svc.Pattern)
|
||||
for i, part := range parts {
|
||||
if len(part) < 1 {
|
||||
return ErrInvalidPattern
|
||||
}
|
||||
|
||||
// not directly preceded by a slash
|
||||
if svc.Pattern[c-1] != '/' {
|
||||
return ErrInvalidPatternBracePosition
|
||||
// if brace capture
|
||||
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||
braceName := matches[0][1]
|
||||
|
||||
// append
|
||||
if svc.Captures == nil {
|
||||
svc.Captures = make([]*BraceCapture, 0)
|
||||
}
|
||||
depth++
|
||||
svc.Captures = append(svc.Captures, &BraceCapture{
|
||||
Index: i,
|
||||
Name: braceName,
|
||||
Ref: nil,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if char == '}' {
|
||||
// closing brace when already closed
|
||||
if depth != 1 {
|
||||
return ErrInvalidPatternClosingBrace
|
||||
}
|
||||
// not directly followed by a slash or end of pattern
|
||||
if c+1 < l && svc.Pattern[c+1] != '/' {
|
||||
return ErrInvalidPatternBracePosition
|
||||
}
|
||||
depth--
|
||||
|
||||
// fail on invalid format
|
||||
if strings.ContainsAny(part, "{}") {
|
||||
return ErrInvalidPatternBraceCapture
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
||||
func (svc *Service) checkAndFormatInput(types []datatype.T) error {
|
||||
|
||||
// ignore no parameter
|
||||
if svc.Input == nil || len(svc.Input) < 1 {
|
||||
|
@ -93,12 +97,45 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
|||
|
||||
// for each parameter
|
||||
for paramName, param := range svc.Input {
|
||||
|
||||
// fail on invalid name
|
||||
if strings.Trim(paramName, "_") != paramName {
|
||||
if len(paramName) < 1 {
|
||||
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||
}
|
||||
|
||||
// fail if brace capture does not exists in pattern
|
||||
iscapture := false
|
||||
if matches := braceRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||
braceName := matches[0][1]
|
||||
|
||||
found := false
|
||||
for _, capture := range svc.Captures {
|
||||
if capture.Name == braceName {
|
||||
capture.Ref = param
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture)
|
||||
}
|
||||
iscapture = true
|
||||
|
||||
} else if matches := queryRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||
|
||||
queryName := matches[0][1]
|
||||
|
||||
// init map
|
||||
if svc.Query == nil {
|
||||
svc.Query = make(map[string]*Parameter)
|
||||
}
|
||||
svc.Query[queryName] = param
|
||||
|
||||
} else {
|
||||
if svc.Form == nil {
|
||||
svc.Form = make(map[string]*Parameter)
|
||||
}
|
||||
svc.Form[paramName] = param
|
||||
}
|
||||
|
||||
// use param name if no rename
|
||||
if len(param.Rename) < 1 {
|
||||
param.Rename = paramName
|
||||
|
@ -109,6 +146,11 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
|||
return fmt.Errorf("%s: %w", paramName, err)
|
||||
}
|
||||
|
||||
// capture parameter cannot be optional
|
||||
if iscapture && param.Optional {
|
||||
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
|
||||
}
|
||||
|
||||
if !param.assignDataType(types) {
|
||||
return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType)
|
||||
}
|
||||
|
@ -136,8 +178,8 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
|||
|
||||
// checks if an uri matches the service's pattern
|
||||
func (svc *Service) matchPattern(uri string) bool {
|
||||
uriparts := splitURL(uri)
|
||||
parts := splitURL(svc.Pattern)
|
||||
uriparts := SplitURL(uri)
|
||||
parts := SplitURL(svc.Pattern)
|
||||
|
||||
// fail if size differ
|
||||
if len(uriparts) != len(parts) {
|
|
@ -3,11 +3,39 @@ package config
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/go/aicra/config/datatype"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
)
|
||||
|
||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||
|
||||
// Server represents a full server configuration
|
||||
type Server struct {
|
||||
Types []datatype.T
|
||||
Services []*Service
|
||||
}
|
||||
|
||||
// Service represents a service definition (from api.json)
|
||||
type Service struct {
|
||||
Method string `json:"method"`
|
||||
Pattern string `json:"path"`
|
||||
Scope [][]string `json:"scope"`
|
||||
Description string `json:"info"`
|
||||
Input map[string]*Parameter `json:"in"`
|
||||
// Download *bool `json:"download"`
|
||||
// Output map[string]*Parameter `json:"out"`
|
||||
|
||||
// references to url parameters
|
||||
// format: '/uri/{param}'
|
||||
Captures []*BraceCapture
|
||||
|
||||
// references to Query parameters
|
||||
// format: 'GET@paranName'
|
||||
Query map[string]*Parameter
|
||||
|
||||
// references for form parameters (all but Captures and Query)
|
||||
Form map[string]*Parameter
|
||||
}
|
||||
|
||||
// Parameter represents a parameter definition (from api.json)
|
||||
type Parameter struct {
|
||||
Description string `json:"info"`
|
||||
|
@ -20,19 +48,9 @@ type Parameter struct {
|
|||
Validator datatype.Validator
|
||||
}
|
||||
|
||||
// Service represents a service definition (from api.json)
|
||||
type Service struct {
|
||||
Method string `json:"method"`
|
||||
Pattern string `json:"path"`
|
||||
Scope [][]string `json:"scope"`
|
||||
Description string `json:"info"`
|
||||
Input map[string]*Parameter `json:"in"`
|
||||
// Download *bool `json:"download"`
|
||||
// Output map[string]*Parameter `json:"out"`
|
||||
}
|
||||
|
||||
// Server represents a full server configuration
|
||||
type Server struct {
|
||||
types []datatype.DataType
|
||||
services []*Service
|
||||
// BraceCapture links to the related URI parameter
|
||||
type BraceCapture struct {
|
||||
Name string
|
||||
Index int
|
||||
Ref *Parameter
|
||||
}
|
|
@ -1,15 +1,21 @@
|
|||
package multipart
|
||||
|
||||
import "git.xdrm.io/go/aicra/internal/cerr"
|
||||
// Error allows you to create constant "const" error with type boxing.
|
||||
type Error string
|
||||
|
||||
// Error implements the error builtin interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
||||
const ErrMissingDataName = cerr.Error("data has no name")
|
||||
const ErrMissingDataName = Error("data has no name")
|
||||
|
||||
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
||||
const ErrDataNameConflict = cerr.Error("data name conflict")
|
||||
const ErrDataNameConflict = Error("data name conflict")
|
||||
|
||||
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
||||
const ErrNoHeader = cerr.Error("data has no header")
|
||||
const ErrNoHeader = Error("data has no header")
|
||||
|
||||
// Component represents a multipart variable/file
|
||||
type Component struct {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package reqdata
|
||||
|
||||
// Error allows you to create constant "const" error with type boxing.
|
||||
type Error string
|
||||
|
||||
// Error implements the error builtin interface.
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
// ErrUnknownType is returned when encountering an unknown type
|
||||
const ErrUnknownType = Error("unknown type")
|
||||
|
||||
// ErrInvalidJSON is returned when json parse failed
|
||||
const ErrInvalidJSON = Error("invalid json")
|
||||
|
||||
// ErrInvalidRootType is returned when json is a map
|
||||
const ErrInvalidRootType = Error("invalid json root type")
|
||||
|
||||
// ErrInvalidParamName - parameter has an invalid
|
||||
const ErrInvalidParamName = Error("invalid parameter name")
|
||||
|
||||
// ErrMissingRequiredParam - required param is missing
|
||||
const ErrMissingRequiredParam = Error("missing required param")
|
||||
|
||||
// ErrInvalidType - parameter value does not satisfy its type
|
||||
const ErrInvalidType = Error("invalid type")
|
||||
|
||||
// ErrMissingURIParameter - missing an URI parameter
|
||||
const ErrMissingURIParameter = Error("missing URI parameter")
|
|
@ -4,19 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"git.xdrm.io/go/aicra/internal/cerr"
|
||||
)
|
||||
|
||||
// ErrUnknownType is returned when encountering an unknown type
|
||||
const ErrUnknownType = cerr.Error("unknown type")
|
||||
|
||||
// ErrInvalidJSON is returned when json parse failed
|
||||
const ErrInvalidJSON = cerr.Error("invalid json")
|
||||
|
||||
// ErrInvalidRootType is returned when json is a map
|
||||
const ErrInvalidRootType = cerr.Error("invalid json root type")
|
||||
|
||||
// Parameter represents an http request parameter
|
||||
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
||||
type Parameter struct {
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
package reqdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.xdrm.io/go/aicra/internal/config"
|
||||
"git.xdrm.io/go/aicra/internal/multipart"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Set represents all data that can be caught:
|
||||
// - URI (from the URI)
|
||||
// - GET (default url data)
|
||||
// - POST (from json, form-data, url-encoded)
|
||||
// - 'application/json' => key-value pair is parsed as json into the map
|
||||
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
||||
// - 'multipart/form-data' => parse form-data format
|
||||
type Set struct {
|
||||
service *config.Service
|
||||
|
||||
// contains URL+GET+FORM data with prefixes:
|
||||
// - FORM: no prefix
|
||||
// - URL: '{uri_var}'
|
||||
// - GET: 'GET@' followed by the key in GET
|
||||
Data map[string]*Parameter
|
||||
}
|
||||
|
||||
// New creates a new empty store.
|
||||
func New(service *config.Service) *Set {
|
||||
return &Set{
|
||||
service: service,
|
||||
Data: make(map[string]*Parameter),
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractURI fills 'Set' with creating pointers inside 'Url'
|
||||
func (i *Set) ExtractURI(req *http.Request) error {
|
||||
uriparts := config.SplitURL(req.URL.RequestURI())
|
||||
|
||||
for _, capture := range i.service.Captures {
|
||||
// out of range
|
||||
if capture.Index > len(uriparts)-1 {
|
||||
return fmt.Errorf("%s: %w", capture.Name, ErrMissingURIParameter)
|
||||
}
|
||||
value := uriparts[capture.Index]
|
||||
|
||||
// should not happen
|
||||
if capture.Ref == nil {
|
||||
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
|
||||
}
|
||||
|
||||
// check type
|
||||
cast, valid := capture.Ref.Validator(value)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
|
||||
}
|
||||
|
||||
// store cast value in 'Set'
|
||||
i.Data[capture.Ref.Rename] = &Parameter{
|
||||
Value: cast,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractQuery data from the url query parameters
|
||||
func (i *Set) ExtractQuery(req *http.Request) error {
|
||||
query := req.URL.Query()
|
||||
|
||||
for name, param := range i.service.Query {
|
||||
value, exist := query[name]
|
||||
|
||||
// fail on missing required
|
||||
if !exist && !param.Optional {
|
||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||
}
|
||||
|
||||
// optional
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
// check type
|
||||
cast, valid := param.Validator(value)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||
}
|
||||
|
||||
// store value
|
||||
i.Data[param.Rename] = &Parameter{
|
||||
Value: cast,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractForm data from request
|
||||
//
|
||||
// - parse 'form-data' if not supported for non-POST requests
|
||||
// - parse 'x-www-form-urlencoded'
|
||||
// - parse 'application/json'
|
||||
func (i *Set) ExtractForm(req *http.Request) error {
|
||||
|
||||
// ignore GET method
|
||||
if req.Method == http.MethodGet {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
|
||||
// parse json
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
return i.parseJSON(req)
|
||||
}
|
||||
|
||||
// parse urlencoded
|
||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
return i.parseUrlencoded(req)
|
||||
}
|
||||
|
||||
// parse multipart
|
||||
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
||||
return i.parseMultipart(req)
|
||||
}
|
||||
|
||||
// nothing to parse
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseJSON parses JSON from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *Set) parseJSON(req *http.Request) error {
|
||||
|
||||
parsed := make(map[string]interface{}, 0)
|
||||
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
if err := decoder.Decode(&parsed); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
|
||||
}
|
||||
|
||||
for name, param := range i.service.Form {
|
||||
value, exist := parsed[name]
|
||||
|
||||
// fail on missing required
|
||||
if !exist && !param.Optional {
|
||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||
}
|
||||
|
||||
// optional
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
// fail on invalid type
|
||||
cast, valid := param.Validator(value)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||
}
|
||||
|
||||
// store value
|
||||
i.Data[param.Rename] = &Parameter{
|
||||
Value: cast,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *Set) parseUrlencoded(req *http.Request) error {
|
||||
// use http.Request interface
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, param := range i.service.Form {
|
||||
value, exist := req.PostForm[name]
|
||||
|
||||
// fail on missing required
|
||||
if !exist && !param.Optional {
|
||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||
}
|
||||
|
||||
// optional
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
// check type
|
||||
cast, valid := param.Validator(value)
|
||||
if !valid {
|
||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||
}
|
||||
|
||||
// store value
|
||||
i.Data[param.Rename] = &Parameter{
|
||||
Value: cast,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseMultipart parses multi-part from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *Set) parseMultipart(req *http.Request) error {
|
||||
|
||||
// 1. create reader
|
||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||
mpr, err := multipart.NewReader(req.Body, boundary)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. parse multipart
|
||||
if err = mpr.Parse(); err != nil {
|
||||
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
||||
}
|
||||
|
||||
for name, param := range i.service.Form {
|
||||
component, exist := mpr.Data[name]
|
||||
|
||||
// fail on missing required
|
||||
if !exist && !param.Optional {
|
||||
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||
}
|
||||
|
||||
// optional
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
// fail on invalid type
|
||||
cast, valid := param.Validator(string(component.Data))
|
||||
if !valid {
|
||||
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||
}
|
||||
|
||||
// store value
|
||||
i.Data[param.Rename] = &Parameter{
|
||||
Value: cast,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
|
@ -0,0 +1,784 @@
|
|||
package reqdata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.xdrm.io/go/aicra/internal/config"
|
||||
)
|
||||
|
||||
func getEmptyService() *config.Service {
|
||||
return &config.Service{}
|
||||
}
|
||||
|
||||
func getServiceWithURI(capturingBraces ...string) *config.Service {
|
||||
service := &config.Service{
|
||||
Input: make(map[string]*config.Parameter),
|
||||
}
|
||||
|
||||
index := 0
|
||||
|
||||
for _, capture := range capturingBraces {
|
||||
if len(capture) == 0 {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("{%s}", capture)
|
||||
service.Input[id] = &config.Parameter{
|
||||
Rename: capture,
|
||||
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||
}
|
||||
|
||||
service.Captures = append(service.Captures, &config.BraceCapture{
|
||||
Name: capture,
|
||||
Index: index,
|
||||
Ref: service.Input[id],
|
||||
})
|
||||
index++
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
func getServiceWithQuery(params ...string) *config.Service {
|
||||
service := &config.Service{
|
||||
Input: make(map[string]*config.Parameter),
|
||||
Query: make(map[string]*config.Parameter),
|
||||
}
|
||||
|
||||
for _, name := range params {
|
||||
id := fmt.Sprintf("GET@%s", name)
|
||||
service.Input[id] = &config.Parameter{
|
||||
Rename: name,
|
||||
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||
}
|
||||
|
||||
service.Query[name] = service.Input[id]
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
func getServiceWithForm(params ...string) *config.Service {
|
||||
service := &config.Service{
|
||||
Input: make(map[string]*config.Parameter),
|
||||
Form: make(map[string]*config.Parameter),
|
||||
}
|
||||
|
||||
for _, name := range params {
|
||||
service.Input[name] = &config.Parameter{
|
||||
Rename: name,
|
||||
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||
}
|
||||
|
||||
service.Form[name] = service.Input[name]
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func TestStoreWithUri(t *testing.T) {
|
||||
tests := []struct {
|
||||
ServiceParams []string
|
||||
URI string
|
||||
Err error
|
||||
}{
|
||||
{
|
||||
[]string{},
|
||||
"/non-captured/uri",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]string{"missing"},
|
||||
"/",
|
||||
ErrMissingURIParameter,
|
||||
},
|
||||
{
|
||||
[]string{"gotit", "missing"},
|
||||
"/gotme",
|
||||
ErrMissingURIParameter,
|
||||
},
|
||||
{
|
||||
[]string{"gotit", "gotittoo"},
|
||||
"/gotme/andme",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]string{"gotit", "gotittoo"},
|
||||
"/gotme/andme/ignored",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]string{"first", "", "second"},
|
||||
"/gotme/ignored/gotmetoo",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]string{"first", "", "second"},
|
||||
"/gotme/ignored",
|
||||
ErrMissingURIParameter,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test.%d", i), func(t *testing.T) {
|
||||
service := getServiceWithURI(test.ServiceParams...)
|
||||
store := New(service)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
||||
err := store.ExtractURI(req)
|
||||
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()
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected error <%s>", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if len(store.Data) != len(service.Input) {
|
||||
t.Errorf("store should contain %d elements, got %d", len(service.Input), len(store.Data))
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtractQuery(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
ServiceParam []string
|
||||
Query string
|
||||
Err error
|
||||
|
||||
ParamNames []string
|
||||
ParamValues [][]string
|
||||
}{
|
||||
{
|
||||
ServiceParam: []string{},
|
||||
Query: "",
|
||||
Err: nil,
|
||||
ParamNames: nil,
|
||||
ParamValues: nil,
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"missing"},
|
||||
Query: "",
|
||||
Err: ErrMissingRequiredParam,
|
||||
ParamNames: nil,
|
||||
ParamValues: nil,
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a"},
|
||||
Query: "a",
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a"},
|
||||
Query: "a&b",
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a", "missing"},
|
||||
Query: "a&b",
|
||||
Err: ErrMissingRequiredParam,
|
||||
ParamNames: nil,
|
||||
ParamValues: nil,
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a", "b"},
|
||||
Query: "a&b",
|
||||
Err: nil,
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a"},
|
||||
Err: nil,
|
||||
Query: "a=",
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a", "b"},
|
||||
Err: nil,
|
||||
Query: "a=&b=x",
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||
},
|
||||
{
|
||||
ServiceParam: []string{"a", "c"},
|
||||
Err: nil,
|
||||
Query: "a=b&c=d",
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{"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"}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
|
||||
store := New(getServiceWithQuery(test.ServiceParam...))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
||||
err := store.ExtractQuery(req)
|
||||
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()
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected error <%s>", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
for pi, pName := range test.ParamNames {
|
||||
values := test.ParamValues[pi]
|
||||
|
||||
t.Run(pName, func(t *testing.T) {
|
||||
param, isset := store.Data[pName]
|
||||
if !isset {
|
||||
t.Errorf("param does not exist")
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
cast, canCast := param.Value.([]string)
|
||||
if !canCast {
|
||||
t.Errorf("should return a []string (got '%v')", cast)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if len(cast) != len(values) {
|
||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||
// http.Request.ParseForm() fails when:
|
||||
// - http.Request.Method is one of [POST,PUT,PATCH]
|
||||
// - http.Request.Form is not nil (created manually)
|
||||
// - http.Request.PostForm is nil (deleted manually)
|
||||
// - http.Request.Body is nil (deleted manually)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// break everything
|
||||
req.Body = nil
|
||||
req.Form = make(url.Values)
|
||||
req.PostForm = nil
|
||||
|
||||
// defer req.Body.Close()
|
||||
store := New(nil)
|
||||
err := store.ExtractForm(req)
|
||||
if err == nil {
|
||||
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
||||
t.FailNow()
|
||||
|
||||
}
|
||||
}
|
||||
func TestExtractFormUrlEncoded(t *testing.T) {
|
||||
tests := []struct {
|
||||
ServiceParams []string
|
||||
URLEncoded string
|
||||
Err error
|
||||
|
||||
ParamNames []string
|
||||
ParamValues [][]string
|
||||
}{
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
URLEncoded: "",
|
||||
Err: nil,
|
||||
ParamNames: nil,
|
||||
ParamValues: nil,
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"missing"},
|
||||
URLEncoded: "",
|
||||
Err: ErrMissingRequiredParam,
|
||||
ParamNames: nil,
|
||||
ParamValues: nil,
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
URLEncoded: "a",
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
URLEncoded: "a&b",
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a", "missing"},
|
||||
URLEncoded: "a&b",
|
||||
Err: ErrMissingRequiredParam,
|
||||
ParamNames: nil,
|
||||
ParamValues: nil,
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a", "b"},
|
||||
URLEncoded: "a&b",
|
||||
Err: nil,
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
Err: nil,
|
||||
URLEncoded: "a=",
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a", "b"},
|
||||
Err: nil,
|
||||
URLEncoded: "a=&b=x",
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a", "c"},
|
||||
Err: nil,
|
||||
URLEncoded: "a=b&c=d",
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{"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"}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
body := strings.NewReader(test.URLEncoded)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
defer req.Body.Close()
|
||||
|
||||
store := New(getServiceWithForm(test.ServiceParams...))
|
||||
err := store.ExtractForm(req)
|
||||
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()
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected error <%s>", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
for pi, key := range test.ParamNames {
|
||||
values := test.ParamValues[pi]
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
param, isset := store.Data[key]
|
||||
if !isset {
|
||||
t.Errorf("param does not exist")
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
cast, canCast := param.Value.([]string)
|
||||
if !canCast {
|
||||
t.Errorf("should return a []string (got '%v')", cast)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if len(cast) != len(values) {
|
||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestJsonParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
ServiceParams []string
|
||||
Raw string
|
||||
Err error
|
||||
|
||||
ParamNames []string
|
||||
ParamValues []interface{}
|
||||
}{
|
||||
// no need to fully check json because it is parsed with the standard library
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
Raw: "",
|
||||
Err: nil,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
Raw: "{}",
|
||||
Err: nil,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
Raw: `{ "a": "b" }`,
|
||||
Err: nil,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
Raw: `{ "a": "b" }`,
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{"b"},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
Raw: `{ "a": "b", "ignored": "d" }`,
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{"b"},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a", "c"},
|
||||
Raw: `{ "a": "b", "c": "d" }`,
|
||||
Err: nil,
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: []interface{}{"b", "d"},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
Raw: `{ "a": null }`,
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
// json parse error
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
Raw: `{ "a": "b", }`,
|
||||
Err: ErrInvalidJSON,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
body := strings.NewReader(test.Raw)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
defer req.Body.Close()
|
||||
store := New(getServiceWithForm(test.ServiceParams...))
|
||||
|
||||
err := store.ExtractForm(req)
|
||||
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()
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected error <%s>", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
for pi, pName := range test.ParamNames {
|
||||
key := pName
|
||||
value := test.ParamValues[pi]
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
param, isset := store.Data[key]
|
||||
if !isset {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
|
||||
paramValue := param.Value
|
||||
paramValueType := reflect.TypeOf(param.Value)
|
||||
|
||||
if valueType != paramValueType {
|
||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if paramValue != value {
|
||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMultipartParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
ServiceParams []string
|
||||
RawMultipart string
|
||||
Err error
|
||||
|
||||
ParamNames []string
|
||||
ParamValues []interface{}
|
||||
}{
|
||||
// no need to fully check json because it is parsed with the standard library
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
RawMultipart: ``,
|
||||
Err: nil,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
RawMultipart: `--xxx
|
||||
`,
|
||||
Err: ErrInvalidMultipart,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
RawMultipart: `--xxx
|
||||
--xxx--`,
|
||||
Err: ErrInvalidMultipart,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{},
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx--`,
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx--`,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{"b"},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a", "c"},
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="c"
|
||||
|
||||
d
|
||||
--xxx--`,
|
||||
Err: nil,
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: []interface{}{"b", "d"},
|
||||
},
|
||||
{
|
||||
ServiceParams: []string{"a"},
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="ignored"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
Err: nil,
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{"b"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
body := strings.NewReader(test.RawMultipart)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
||||
defer req.Body.Close()
|
||||
store := New(getServiceWithForm(test.ServiceParams...))
|
||||
|
||||
err := store.ExtractForm(req)
|
||||
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()
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected error <%s>", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
for pi, key := range test.ParamNames {
|
||||
value := test.ParamValues[pi]
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
param, isset := store.Data[key]
|
||||
if !isset {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
|
||||
paramValue := param.Value
|
||||
paramValueType := reflect.TypeOf(param.Value)
|
||||
|
||||
if valueType != paramValueType {
|
||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if paramValue != value {
|
||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,301 +0,0 @@
|
|||
package reqdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.xdrm.io/go/aicra/internal/multipart"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Store represents all data that can be caught:
|
||||
// - URI (guessed from the URI by removing the service path)
|
||||
// - GET (default url data)
|
||||
// - POST (from json, form-data, url-encoded)
|
||||
type Store struct {
|
||||
|
||||
// ordered values from the URI
|
||||
// catches all after the service path
|
||||
//
|
||||
// points to Store.Data
|
||||
URI []*Parameter
|
||||
|
||||
// uri parameters following the QUERY format
|
||||
//
|
||||
// points to Store.Data
|
||||
Get map[string]*Parameter
|
||||
|
||||
// form data depending on the Content-Type:
|
||||
// 'application/json' => key-value pair is parsed as json into the map
|
||||
// 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
||||
// 'multipart/form-data' => parse form-data format
|
||||
//
|
||||
// points to Store.Data
|
||||
Form map[string]*Parameter
|
||||
|
||||
// contains URL+GET+FORM data with prefixes:
|
||||
// - FORM: no prefix
|
||||
// - URL: 'URL#' followed by the index in Uri
|
||||
// - GET: 'GET@' followed by the key in GET
|
||||
Set map[string]*Parameter
|
||||
}
|
||||
|
||||
// New creates a new store from an http request.
|
||||
// URI params is required because it only takes into account after service path
|
||||
// we do not know in this scope.
|
||||
func New(uriParams []string, req *http.Request) *Store {
|
||||
ds := &Store{
|
||||
URI: make([]*Parameter, 0),
|
||||
Get: make(map[string]*Parameter),
|
||||
Form: make(map[string]*Parameter),
|
||||
Set: make(map[string]*Parameter),
|
||||
}
|
||||
|
||||
// 1. set URI parameters
|
||||
ds.setURIParams(uriParams)
|
||||
|
||||
// ignore nil requests
|
||||
if req == nil {
|
||||
return ds
|
||||
}
|
||||
|
||||
// 2. GET (query) data
|
||||
ds.readQuery(req)
|
||||
|
||||
// 3. We are done if GET method
|
||||
if req.Method == http.MethodGet {
|
||||
return ds
|
||||
}
|
||||
|
||||
// 4. POST (body) data
|
||||
ds.readForm(req)
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// setURIParameters fills 'Set' with creating pointers inside 'Url'
|
||||
func (i *Store) setURIParams(orderedUParams []string) {
|
||||
|
||||
for index, value := range orderedUParams {
|
||||
|
||||
// create set index
|
||||
setindex := fmt.Sprintf("URL#%d", index)
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[setindex] = &Parameter{
|
||||
Parsed: false,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Url'
|
||||
i.URI = append(i.URI, i.Set[setindex])
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// readQuery stores data from the QUERY (in url parameters)
|
||||
func (i *Store) readQuery(req *http.Request) {
|
||||
|
||||
for name, value := range req.URL.Query() {
|
||||
|
||||
// prevent invalid names
|
||||
if !isNameValid(name) {
|
||||
log.Printf("invalid variable name: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// prevent injections
|
||||
if hasNameInjection(name) {
|
||||
log.Printf("get.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// create set index
|
||||
setindex := fmt.Sprintf("GET@%s", name)
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[setindex] = &Parameter{
|
||||
Parsed: false,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Get'
|
||||
i.Get[name] = i.Set[setindex]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// readForm stores FORM data
|
||||
//
|
||||
// - parse 'form-data' if not supported (not POST requests)
|
||||
// - parse 'x-www-form-urlencoded'
|
||||
// - parse 'application/json'
|
||||
func (i *Store) readForm(req *http.Request) {
|
||||
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
|
||||
// parse json
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
i.parseJSON(req)
|
||||
return
|
||||
}
|
||||
|
||||
// parse urlencoded
|
||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
i.parseUrlencoded(req)
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart
|
||||
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
||||
i.parseMultipart(req)
|
||||
return
|
||||
}
|
||||
|
||||
// if unknown type store nothing
|
||||
}
|
||||
|
||||
// parseJSON parses JSON from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *Store) parseJSON(req *http.Request) {
|
||||
|
||||
parsed := make(map[string]interface{}, 0)
|
||||
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
|
||||
// if parse error: do nothing
|
||||
if err := decoder.Decode(&parsed); err != nil {
|
||||
log.Printf("json.parse() %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// else store values 'parsed' values
|
||||
for name, value := range parsed {
|
||||
|
||||
// prevent invalid names
|
||||
if !isNameValid(name) {
|
||||
log.Printf("invalid variable name: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// prevent injections
|
||||
if hasNameInjection(name) {
|
||||
log.Printf("post.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[name] = &Parameter{
|
||||
Parsed: true,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Form'
|
||||
i.Form[name] = i.Set[name]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *Store) parseUrlencoded(req *http.Request) {
|
||||
|
||||
// use http.Request interface
|
||||
if err := req.ParseForm(); err != nil {
|
||||
log.Printf("urlencoded.parse() %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for name, value := range req.PostForm {
|
||||
|
||||
// prevent invalid names
|
||||
if !isNameValid(name) {
|
||||
log.Printf("invalid variable name: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// prevent injections
|
||||
if hasNameInjection(name) {
|
||||
log.Printf("post.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[name] = &Parameter{
|
||||
Parsed: false,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Form'
|
||||
i.Form[name] = i.Set[name]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// parseMultipart parses multi-part from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *Store) parseMultipart(req *http.Request) {
|
||||
|
||||
/* (1) Create reader */
|
||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||
mpr, err := multipart.NewReader(req.Body, boundary)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
/* (2) Parse multipart */
|
||||
if err = mpr.Parse(); err != nil {
|
||||
log.Printf("multipart.parse() %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
/* (3) Store data into 'Form' and 'Set */
|
||||
for name, data := range mpr.Data {
|
||||
|
||||
// prevent invalid names
|
||||
if !isNameValid(name) {
|
||||
log.Printf("invalid variable name: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// prevent injections
|
||||
if hasNameInjection(name) {
|
||||
log.Printf("post.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[name] = &Parameter{
|
||||
Parsed: false,
|
||||
File: len(data.GetHeader("filename")) > 0,
|
||||
Value: string(data.Data),
|
||||
}
|
||||
|
||||
// create link in 'Form'
|
||||
i.Form[name] = i.Set[name]
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// hasNameInjection returns whether there is
|
||||
// a parameter name injection:
|
||||
// - inferred GET parameters
|
||||
// - inferred URL parameters
|
||||
func hasNameInjection(pName string) bool {
|
||||
return strings.HasPrefix(pName, "GET@") || strings.HasPrefix(pName, "URL#")
|
||||
}
|
||||
|
||||
// isNameValid returns whether a parameter name (without the GET@ or URL# prefix) is valid
|
||||
// if fails if the name begins/ends with underscores
|
||||
func isNameValid(pName string) bool {
|
||||
return strings.Trim(pName, "_") == pName
|
||||
}
|
|
@ -1,804 +0,0 @@
|
|||
package reqdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmptyStore(t *testing.T) {
|
||||
store := New(nil, nil)
|
||||
|
||||
if store.URI == nil {
|
||||
t.Errorf("store 'URI' list should be initialized")
|
||||
t.Fail()
|
||||
}
|
||||
if len(store.URI) != 0 {
|
||||
t.Errorf("store 'URI' list should be empty")
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if store.Get == nil {
|
||||
t.Errorf("store 'Get' map should be initialized")
|
||||
t.Fail()
|
||||
}
|
||||
if store.Form == nil {
|
||||
t.Errorf("store 'Form' map should be initialized")
|
||||
t.Fail()
|
||||
}
|
||||
if store.Set == nil {
|
||||
t.Errorf("store 'Set' map should be initialized")
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreWithUri(t *testing.T) {
|
||||
urilist := []string{"abc", "def"}
|
||||
store := New(urilist, nil)
|
||||
|
||||
if len(store.URI) != len(urilist) {
|
||||
t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.URI))
|
||||
t.Fail()
|
||||
}
|
||||
if len(store.Set) != len(urilist) {
|
||||
t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.Set))
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
for i, value := range urilist {
|
||||
|
||||
t.Run(fmt.Sprintf("URL#%d='%s'", i, value), func(t *testing.T) {
|
||||
key := fmt.Sprintf("URL#%d", i)
|
||||
element, isset := store.Set[key]
|
||||
|
||||
if !isset {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
if element.Value != value {
|
||||
t.Errorf("store[%s] should return '%s' (got '%s')", key, value, element.Value)
|
||||
t.Failed()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStoreWithGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
Query string
|
||||
|
||||
InvalidNames []string
|
||||
ParamNames []string
|
||||
ParamValues [][]string
|
||||
}{
|
||||
{
|
||||
Query: "",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: [][]string{},
|
||||
},
|
||||
{
|
||||
Query: "a",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
Query: "a&b",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||
},
|
||||
{
|
||||
Query: "a=",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
Query: "a=&b=x",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||
},
|
||||
{
|
||||
Query: "a=b&c=d",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||
},
|
||||
{
|
||||
Query: "a=b&c=d&a=x",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||
},
|
||||
{
|
||||
Query: "a=b&_invalid=x",
|
||||
InvalidNames: []string{"_invalid"},
|
||||
ParamNames: []string{"a", "_invalid"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
{
|
||||
Query: "a=b&invalid_=x",
|
||||
InvalidNames: []string{"invalid_"},
|
||||
ParamNames: []string{"a", "invalid_"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
{
|
||||
Query: "a=b&GET@injection=x",
|
||||
InvalidNames: []string{"GET@injection"},
|
||||
ParamNames: []string{"a", "GET@injection"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
{ // not really useful as all after '#' should be ignored by http clients
|
||||
Query: "a=b&URL#injection=x",
|
||||
InvalidNames: []string{"URL#injection"},
|
||||
ParamNames: []string{"a", "URL#injection"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
||||
store := New(nil, req)
|
||||
|
||||
if test.ParamNames == nil || test.ParamValues == nil {
|
||||
if len(store.Set) != 0 {
|
||||
t.Errorf("expected no GET parameters and got %d", len(store.Get))
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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.Failed()
|
||||
}
|
||||
|
||||
for pi, pName := range test.ParamNames {
|
||||
key := fmt.Sprintf("GET@%s", pName)
|
||||
values := test.ParamValues[pi]
|
||||
|
||||
isNameValid := true
|
||||
for _, invalid := range test.InvalidNames {
|
||||
if pName == invalid {
|
||||
isNameValid = false
|
||||
}
|
||||
}
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
param, isset := store.Set[key]
|
||||
if !isset {
|
||||
if isNameValid {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.Failed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if should be invalid
|
||||
if isset && !isNameValid {
|
||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
cast, canCast := param.Value.([]string)
|
||||
|
||||
if !canCast {
|
||||
t.Errorf("should return a []string (got '%v')", cast)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
if len(cast) != len(values) {
|
||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
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.Failed()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||
// http.Request.ParseForm() fails when:
|
||||
// - http.Request.Method is one of [POST,PUT,PATCH]
|
||||
// - http.Request.Form is not nil (created manually)
|
||||
// - http.Request.PostForm is nil (deleted manually)
|
||||
// - http.Request.Body is nil (deleted manually)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// break everything
|
||||
req.Body = nil
|
||||
req.Form = make(url.Values)
|
||||
req.PostForm = nil
|
||||
|
||||
// defer req.Body.Close()
|
||||
store := New(nil, req)
|
||||
if len(store.Form) > 0 {
|
||||
t.Errorf("expected malformed urlencoded to have failed being parsed (got %d elements)", len(store.Form))
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
func TestStoreWithUrlEncodedForm(t *testing.T) {
|
||||
tests := []struct {
|
||||
URLEncoded string
|
||||
|
||||
InvalidNames []string
|
||||
ParamNames []string
|
||||
ParamValues [][]string
|
||||
}{
|
||||
{
|
||||
URLEncoded: "",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: [][]string{},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a&b",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: [][]string{[]string{""}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=&b=x",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "b"},
|
||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=b&c=d",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=b&c=d&a=x",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=b&_invalid=x",
|
||||
InvalidNames: []string{"_invalid"},
|
||||
ParamNames: []string{"a", "_invalid"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=b&invalid_=x",
|
||||
InvalidNames: []string{"invalid_"},
|
||||
ParamNames: []string{"a", "invalid_"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=b&GET@injection=x",
|
||||
InvalidNames: []string{"GET@injection"},
|
||||
ParamNames: []string{"a", "GET@injection"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
{
|
||||
URLEncoded: "a=b&URL#injection=x",
|
||||
InvalidNames: []string{"URL#injection"},
|
||||
ParamNames: []string{"a", "URL#injection"},
|
||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
body := strings.NewReader(test.URLEncoded)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
defer req.Body.Close()
|
||||
store := New(nil, req)
|
||||
|
||||
if test.ParamNames == nil || test.ParamValues == nil {
|
||||
if len(store.Set) != 0 {
|
||||
t.Errorf("expected no FORM parameters and got %d", len(store.Get))
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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.Failed()
|
||||
}
|
||||
|
||||
for pi, pName := range test.ParamNames {
|
||||
key := pName
|
||||
values := test.ParamValues[pi]
|
||||
|
||||
isNameValid := true
|
||||
for _, invalid := range test.InvalidNames {
|
||||
if pName == invalid {
|
||||
isNameValid = false
|
||||
}
|
||||
}
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
param, isset := store.Set[key]
|
||||
if !isset {
|
||||
if isNameValid {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.Failed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if should be invalid
|
||||
if isset && !isNameValid {
|
||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
cast, canCast := param.Value.([]string)
|
||||
|
||||
if !canCast {
|
||||
t.Errorf("should return a []string (got '%v')", cast)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
if len(cast) != len(values) {
|
||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
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.Failed()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestJsonParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
RawJson string
|
||||
|
||||
InvalidNames []string
|
||||
ParamNames []string
|
||||
ParamValues []interface{}
|
||||
}{
|
||||
// no need to fully check json because it is parsed with the standard library
|
||||
{
|
||||
RawJson: "",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
RawJson: "{}",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\" }",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{"b"},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\", \"c\": \"d\" }",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: []interface{}{"b", "d"},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"_invalid\": \"x\" }",
|
||||
InvalidNames: []string{"_invalid"},
|
||||
ParamNames: []string{"_invalid"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\", \"_invalid\": \"x\" }",
|
||||
InvalidNames: []string{"_invalid"},
|
||||
ParamNames: []string{"a", "_invalid"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
|
||||
{
|
||||
RawJson: "{ \"invalid_\": \"x\" }",
|
||||
InvalidNames: []string{"invalid_"},
|
||||
ParamNames: []string{"invalid_"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\", \"invalid_\": \"x\" }",
|
||||
InvalidNames: []string{"invalid_"},
|
||||
ParamNames: []string{"a", "invalid_"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
|
||||
{
|
||||
RawJson: "{ \"GET@injection\": \"x\" }",
|
||||
InvalidNames: []string{"GET@injection"},
|
||||
ParamNames: []string{"GET@injection"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\", \"GET@injection\": \"x\" }",
|
||||
InvalidNames: []string{"GET@injection"},
|
||||
ParamNames: []string{"a", "GET@injection"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
|
||||
{
|
||||
RawJson: "{ \"URL#injection\": \"x\" }",
|
||||
InvalidNames: []string{"URL#injection"},
|
||||
ParamNames: []string{"URL#injection"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\", \"URL#injection\": \"x\" }",
|
||||
InvalidNames: []string{"URL#injection"},
|
||||
ParamNames: []string{"a", "URL#injection"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
// json parse error
|
||||
{
|
||||
RawJson: "{ \"a\": \"b\", }",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
body := strings.NewReader(test.RawJson)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
defer req.Body.Close()
|
||||
store := New(nil, req)
|
||||
|
||||
if test.ParamNames == nil || test.ParamValues == nil {
|
||||
if len(store.Set) != 0 {
|
||||
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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.Failed()
|
||||
}
|
||||
|
||||
for pi, pName := range test.ParamNames {
|
||||
key := pName
|
||||
value := test.ParamValues[pi]
|
||||
|
||||
isNameValid := true
|
||||
for _, invalid := range test.InvalidNames {
|
||||
if pName == invalid {
|
||||
isNameValid = false
|
||||
}
|
||||
}
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
param, isset := store.Set[key]
|
||||
if !isset {
|
||||
if isNameValid {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.Failed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if should be invalid
|
||||
if isset && !isNameValid {
|
||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
|
||||
paramValue := param.Value
|
||||
paramValueType := reflect.TypeOf(param.Value)
|
||||
|
||||
if valueType != paramValueType {
|
||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
if paramValue != value {
|
||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMultipartParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
RawMultipart string
|
||||
|
||||
InvalidNames []string
|
||||
ParamNames []string
|
||||
ParamValues []interface{}
|
||||
}{
|
||||
// no need to fully check json because it is parsed with the standard library
|
||||
{
|
||||
RawMultipart: ``,
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
`,
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
--xxx--`,
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx--`,
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a"},
|
||||
ParamValues: []interface{}{"b"},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="c"
|
||||
|
||||
d
|
||||
--xxx--`,
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{"a", "c"},
|
||||
ParamValues: []interface{}{"b", "d"},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="_invalid"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"_invalid"},
|
||||
ParamNames: []string{"_invalid"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="_invalid"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"_invalid"},
|
||||
ParamNames: []string{"a", "_invalid"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="invalid_"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"invalid_"},
|
||||
ParamNames: []string{"invalid_"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="invalid_"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"invalid_"},
|
||||
ParamNames: []string{"a", "invalid_"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="GET@injection"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"GET@injection"},
|
||||
ParamNames: []string{"GET@injection"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="GET@injection"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"GET@injection"},
|
||||
ParamNames: []string{"a", "GET@injection"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="URL#injection"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"URL#injection"},
|
||||
ParamNames: []string{"URL#injection"},
|
||||
ParamValues: []interface{}{nil},
|
||||
},
|
||||
{
|
||||
RawMultipart: `--xxx
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="URL#injection"
|
||||
|
||||
x
|
||||
--xxx--`,
|
||||
InvalidNames: []string{"URL#injection"},
|
||||
ParamNames: []string{"a", "URL#injection"},
|
||||
ParamValues: []interface{}{"b", nil},
|
||||
},
|
||||
// json parse error
|
||||
{
|
||||
RawMultipart: "{ \"a\": \"b\", }",
|
||||
InvalidNames: []string{},
|
||||
ParamNames: []string{},
|
||||
ParamValues: []interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||
body := strings.NewReader(test.RawMultipart)
|
||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
||||
defer req.Body.Close()
|
||||
store := New(nil, req)
|
||||
|
||||
if test.ParamNames == nil || test.ParamValues == nil {
|
||||
if len(store.Set) != 0 {
|
||||
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
// no param to check
|
||||
return
|
||||
}
|
||||
|
||||
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.Failed()
|
||||
}
|
||||
|
||||
for pi, pName := range test.ParamNames {
|
||||
key := pName
|
||||
value := test.ParamValues[pi]
|
||||
|
||||
isNameValid := true
|
||||
for _, invalid := range test.InvalidNames {
|
||||
if pName == invalid {
|
||||
isNameValid = false
|
||||
}
|
||||
}
|
||||
|
||||
t.Run(key, func(t *testing.T) {
|
||||
|
||||
param, isset := store.Set[key]
|
||||
if !isset {
|
||||
if isNameValid {
|
||||
t.Errorf("store should contain element with key '%s'", key)
|
||||
t.Failed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if should be invalid
|
||||
if isset && !isNameValid {
|
||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
valueType := reflect.TypeOf(value)
|
||||
|
||||
paramValue := param.Value
|
||||
paramValueType := reflect.TypeOf(param.Value)
|
||||
|
||||
if valueType != paramValueType {
|
||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
if paramValue != value {
|
||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||
t.Failed()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package typecheck
|
||||
|
||||
// Set of type checkers
|
||||
type Set struct {
|
||||
types []Type
|
||||
}
|
||||
|
||||
// New returns a new set of type checkers
|
||||
func New() *Set {
|
||||
return &Set{types: make([]Type, 0)}
|
||||
}
|
||||
|
||||
// Add adds a new type checker
|
||||
func (s *Set) Add(typeChecker Type) {
|
||||
s.types = append(s.types, typeChecker)
|
||||
}
|
||||
|
||||
// Run finds a type checker from the registry matching the type `typeName`
|
||||
// and uses this checker to check the `value`. If no type checker matches
|
||||
// the `type`, error is returned by default.
|
||||
func (s *Set) Run(typeName string, value interface{}) error {
|
||||
|
||||
// find matching type (take first)
|
||||
for _, typeChecker := range s.types {
|
||||
if typeChecker == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// found
|
||||
checkerFunc := typeChecker.Checker(typeName)
|
||||
if checkerFunc == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check value
|
||||
if checkerFunc(value) {
|
||||
return nil
|
||||
}
|
||||
return ErrDoesNotMatch
|
||||
}
|
||||
|
||||
return ErrNoMatchingType
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package typecheck
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNoMatchingType when no available type checker matches the type
|
||||
var ErrNoMatchingType = errors.New("no matching type")
|
||||
|
||||
// ErrDoesNotMatch when the value is invalid
|
||||
var ErrDoesNotMatch = errors.New("does not match")
|
||||
|
||||
// CheckerFunc returns whether a given value fulfills a type
|
||||
type CheckerFunc func(interface{}) bool
|
||||
|
||||
// Type represents a type checker
|
||||
type Type interface {
|
||||
// given a type name, returns the checker function or NIL if the type is not handled here
|
||||
Checker(string) CheckerFunc
|
||||
}
|
Loading…
Reference in New Issue