Compare commits

...

18 Commits

Author SHA1 Message Date
Adrien Marquès 3606f9984d
update tests for internal/reqdata set
continuous-integration/drone/push Build is failing Details
2020-03-20 22:36:15 +01:00
Adrien Marquès 7b812c6648
get data from multipart components 2020-03-20 22:35:53 +01:00
Adrien Marquès dc34d9a81a
wrap multipart errors in dedicated error : ErrInvalidMultipart 2020-03-20 22:35:30 +01:00
Adrien Marquès cdbe4cceac
ignore io.EOF while parsing multipart 2020-03-20 22:27:01 +01:00
Adrien Marquès 03d5e87c37
wrap json parser into dedicated error : ErrInvalidJSON 2020-03-20 22:26:43 +01:00
Adrien Marquès c7aa87c660
ignore EOF when parsing form as json 2020-03-20 22:18:34 +01:00
Adrien Marquès 0f62fc25a0
use request.URL.RequestURI() insteaf of request.RequestURI() ; it is not the same 2020-03-20 22:09:38 +01:00
Adrien Marquès 8c539370aa
remove cerr 2020-03-16 12:53:48 +01:00
Adrien Marquès acd0e73438
remove typecheck 2020-03-16 12:53:37 +01:00
Adrien Marquès b38a9a8111
refactor internal/reqdata to work with thew new config 2020-03-16 12:50:30 +01:00
Adrien Marquès 93b31b9718
keep references to Form parameters 2020-03-16 11:48:44 +01:00
Adrien Marquès 12417f7f1c
reference query parameters in config.Service 2020-03-16 11:32:37 +01:00
Adrien Marquès e7f10723a6
check for undefined brace captures + make tests parallel 2020-03-16 10:56:26 +01:00
Adrien Marquès c32b038da2
make splitURL public 2020-03-16 09:26:10 +01:00
Adrien Marquès 1b4922693b
move config -> internal.config and config.datatype to datatype 2020-03-16 09:20:00 +01:00
Adrien Marquès 4e0d669029
make config.Service members public 2020-03-16 09:01:51 +01:00
Adrien Marquès 2c1b9cf5ff
make captures public 2020-03-15 01:38:49 +01:00
Adrien Marquès d1ab4fefb0
add brace captures and check between param and pattern (keep them so no need to check them at each req) 2020-03-15 01:37:28 +01:00
32 changed files with 1344 additions and 1398 deletions

View File

@ -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{}

View File

@ -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{}

View File

@ -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) {

View File

@ -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{}

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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+)\)$`)

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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")

View File

@ -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, "/")

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}
// not directly preceded by a slash
if svc.Pattern[c-1] != '/' {
return ErrInvalidPatternBracePosition
}
depth++
// for each slash-separated chunk
parts := SplitURL(svc.Pattern)
for i, part := range parts {
if len(part) < 1 {
return ErrInvalidPattern
}
if char == '}' {
// closing brace when already closed
if depth != 1 {
return ErrInvalidPatternClosingBrace
// 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)
}
// not directly followed by a slash or end of pattern
if c+1 < l && svc.Pattern[c+1] != '/' {
return ErrInvalidPatternBracePosition
}
depth--
svc.Captures = append(svc.Captures, &BraceCapture{
Index: i,
Name: braceName,
Ref: nil,
})
continue
}
// 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) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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")

View File

@ -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 {

261
internal/reqdata/set.go Normal file
View File

@ -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
}

View File

@ -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()
}
})
}
})
}
}

View File

@ -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
}

View File

@ -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()
}
})
}
})
}
}

View File

@ -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
}

View File

@ -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
}