Merge branch '0.2.0' of go/aicra into master
This commit is contained in:
commit
43c758b615
|
@ -82,13 +82,13 @@ func main() {
|
||||||
server.Checkers.Add( builtin.NewFloat64() );
|
server.Checkers.Add( builtin.NewFloat64() );
|
||||||
|
|
||||||
// 3. bind your implementations
|
// 3. bind your implementations
|
||||||
server.HandleFunc(http.MethodGet, func(req api.Request, res *api.Response){
|
server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){
|
||||||
// ... process stuff ...
|
// ... process stuff ...
|
||||||
res.SetError(api.ErrorSuccess());
|
res.SetError(api.ErrorSuccess());
|
||||||
})
|
})
|
||||||
|
|
||||||
// 4. launch server
|
// 4. launch server
|
||||||
log.Fatal( http.ListenAndServer("localhost:8181", server) )
|
log.Fatal( http.ListenAndServe("localhost:8181", server) )
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -210,6 +210,6 @@ In this example we want 3 arguments :
|
||||||
- [ ] `[a:b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*)
|
- [ ] `[a:b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*)
|
||||||
- [x] generic controllers implementation (shared objects)
|
- [x] generic controllers implementation (shared objects)
|
||||||
- [x] response interface
|
- [x] response interface
|
||||||
- [ ] log bound resources when building the aicra server
|
- [x] log bound resources when building the aicra server
|
||||||
- [ ] fail on check for unimplemented resources at server boot.
|
- [ ] fail on check for unimplemented resources at server boot.
|
||||||
- [ ] fail on check for unavailable types in api.json at server boot.
|
- [ ] fail on check for unavailable types in api.json at server boot.
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
||||||
module git.xdrm.io/go/aicra
|
module git.xdrm.io/go/aicra
|
||||||
|
|
||||||
go 1.12
|
go 1.14
|
||||||
|
|
87
http.go
87
http.go
|
@ -13,100 +13,103 @@ import (
|
||||||
type httpServer Server
|
type httpServer Server
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler and has to be called on each request
|
// ServeHTTP implements http.Handler and has to be called on each request
|
||||||
func (s httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (server httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
// 1. build API request from HTTP request
|
/* (1) create api.Request from http.Request
|
||||||
|
---------------------------------------------------------*/
|
||||||
request, err := api.NewRequest(r)
|
request, err := api.NewRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. find a matching service for this path in the config
|
// 2. find a matching service for this path in the config
|
||||||
serviceDef, pathIndex := s.services.Browse(request.URI)
|
serviceConf, pathIndex := server.config.Browse(request.URI)
|
||||||
if serviceDef == nil {
|
if serviceConf == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. extract the service path from request URI
|
||||||
servicePath := strings.Join(request.URI[:pathIndex], "/")
|
servicePath := strings.Join(request.URI[:pathIndex], "/")
|
||||||
if !strings.HasPrefix(servicePath, "/") {
|
if !strings.HasPrefix(servicePath, "/") {
|
||||||
servicePath = "/" + servicePath
|
servicePath = "/" + servicePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. check if matching methodDef exists in config */
|
// 4. find method configuration from http method */
|
||||||
var methodDef = serviceDef.Method(r.Method)
|
var methodConf = serviceConf.Method(r.Method)
|
||||||
if methodDef == nil {
|
if methodConf == nil {
|
||||||
response := api.NewResponse(api.ErrorUnknownMethod())
|
res := api.NewResponse(api.ErrorUnknownMethod())
|
||||||
response.ServeHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
logError(response)
|
logError(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. parse every input data from the request
|
// 5. parse data from the request (uri, query, form, json)
|
||||||
store := reqdata.New(request.URI[pathIndex:], r)
|
data := reqdata.New(request.URI[pathIndex:], r)
|
||||||
|
|
||||||
/* (4) Check parameters
|
/* (2) check parameters
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
parameters, paramError := s.extractParameters(store, methodDef.Parameters)
|
parameters, paramError := server.extractParameters(data, methodConf.Parameters)
|
||||||
|
|
||||||
// Fail if argument check failed
|
// Fail if argument check failed
|
||||||
if paramError.Code != api.ErrorSuccess().Code {
|
if paramError.Code != api.ErrorSuccess().Code {
|
||||||
response := api.NewResponse(paramError)
|
res := api.NewResponse(paramError)
|
||||||
response.ServeHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
logError(response)
|
logError(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Param = parameters
|
request.Param = parameters
|
||||||
|
|
||||||
/* (5) Search a matching handler
|
/* (3) search for the handler
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
var serviceHandler *api.Handler
|
var foundHandler *api.Handler
|
||||||
var serviceFound bool
|
var found bool
|
||||||
|
|
||||||
for _, handler := range s.handlers {
|
for _, handler := range server.handlers {
|
||||||
if handler.GetPath() == servicePath {
|
if handler.GetPath() == servicePath {
|
||||||
serviceFound = true
|
found = true
|
||||||
if handler.GetMethod() == r.Method {
|
if handler.GetMethod() == r.Method {
|
||||||
serviceHandler = handler
|
foundHandler = handler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail if found no handler
|
// fail if found no handler
|
||||||
if serviceHandler == nil {
|
if foundHandler == nil {
|
||||||
if serviceFound {
|
if found {
|
||||||
response := api.NewResponse()
|
res := api.NewResponse()
|
||||||
response.SetError(api.ErrorUncallableMethod(), servicePath, r.Method)
|
res.SetError(api.ErrorUncallableMethod(), servicePath, r.Method)
|
||||||
response.ServeHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
logError(response)
|
logError(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := api.NewResponse()
|
res := api.NewResponse()
|
||||||
response.SetError(api.ErrorUncallableService(), servicePath)
|
res.SetError(api.ErrorUncallableService(), servicePath)
|
||||||
response.ServeHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
logError(response)
|
logError(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (6) Execute handler and return response
|
/* (4) execute handler and return response
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
// 1. feed request with configuration scope
|
// 1. feed request with configuration scope
|
||||||
request.Scope = methodDef.Scope
|
request.Scope = methodConf.Scope
|
||||||
|
|
||||||
// 1. execute
|
// 2. execute
|
||||||
response := api.NewResponse()
|
res := api.NewResponse()
|
||||||
serviceHandler.Handle(*request, response)
|
foundHandler.Handle(*request, res)
|
||||||
|
|
||||||
// 2. apply headers
|
// 3. apply headers
|
||||||
for key, values := range response.Headers {
|
for key, values := range res.Headers {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
w.Header().Add(key, value)
|
w.Header().Add(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. write to response
|
// 4. write to response
|
||||||
response.ServeHTTP(w, r)
|
res.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,14 @@ func (err Error) Wrap(e error) *WrapError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// WrapError is way to wrap errors recursively.
|
||||||
type WrapError struct {
|
type WrapError struct {
|
||||||
base error
|
base error
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,636 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLegalServiceName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
Error error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"/": {
|
||||||
|
"invalid/service-name": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service-name")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"/": {
|
||||||
|
"invalid/service/name": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service/name")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"/": {
|
||||||
|
"invalid-service-name": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid-service-name")),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"/": {
|
||||||
|
"valid.service_name": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if err == nil && test.Error != nil {
|
||||||
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
if err != nil && test.Error == nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && test.Error != nil {
|
||||||
|
if err.Error() != test.Error.Error() {
|
||||||
|
t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestAvailableMethods(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"GET": { "info": "info" },
|
||||||
|
"POST": { "info": "info" },
|
||||||
|
"PUT": { "info": "info" },
|
||||||
|
"DELETE": { "info": "info" }
|
||||||
|
}`)
|
||||||
|
srv, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error (got '%s')", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.Method(http.MethodGet) == nil {
|
||||||
|
t.Errorf("expected method GET to be available")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if srv.Method(http.MethodPost) == nil {
|
||||||
|
t.Errorf("expected method POST to be available")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if srv.Method(http.MethodPut) == nil {
|
||||||
|
t.Errorf("expected method PUT to be available")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if srv.Method(http.MethodDelete) == nil {
|
||||||
|
t.Errorf("expected method DELETE to be available")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.Method(http.MethodPatch) != nil {
|
||||||
|
t.Errorf("expected method PATH to be UNavailable")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestParseEmpty(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{}`)
|
||||||
|
_, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error (got '%s')", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestParseJsonError(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info
|
||||||
|
},
|
||||||
|
}`) // trailing ',' is invalid JSON
|
||||||
|
_, err := Parse(reader)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
Error error
|
||||||
|
}{
|
||||||
|
{ // missing description
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")),
|
||||||
|
},
|
||||||
|
{ // missing description
|
||||||
|
`{
|
||||||
|
"/": {
|
||||||
|
"subservice": {
|
||||||
|
"GET": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET subservice")),
|
||||||
|
},
|
||||||
|
{ // empty description
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": ""
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")),
|
||||||
|
},
|
||||||
|
{ // valid description
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "a"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // valid description
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "some description"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if err == nil && test.Error != nil {
|
||||||
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
if err != nil && test.Error == nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && test.Error != nil {
|
||||||
|
if err.Error() != test.Error.Error() {
|
||||||
|
t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"original": { "info": "valid-desc", "type": "valid-type", "name": "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
srv, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
method := srv.Method(http.MethodGet)
|
||||||
|
if method == nil {
|
||||||
|
t.Errorf("expected GET method not to be nil")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
for _, param := range method.Parameters {
|
||||||
|
|
||||||
|
if param.Rename != "original" {
|
||||||
|
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestOptionalParam(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"optional": { "info": "valid-desc", "type": "?optional-type" },
|
||||||
|
"required": { "info": "valid-desc", "type": "required-type" },
|
||||||
|
"required2": { "info": "valid-desc", "type": "a" },
|
||||||
|
"optional2": { "info": "valid-desc", "type": "?a" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
srv, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
method := srv.Method(http.MethodGet)
|
||||||
|
if method == nil {
|
||||||
|
t.Errorf("expected GET method not to be nil")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
for pName, param := range method.Parameters {
|
||||||
|
|
||||||
|
if pName == "optional" || pName == "optional2" {
|
||||||
|
if !param.Optional {
|
||||||
|
t.Errorf("expected parameter '%s' to be optional", pName)
|
||||||
|
t.Failed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pName == "required" || pName == "required2" {
|
||||||
|
if param.Optional {
|
||||||
|
t.Errorf("expected parameter '%s' to be required", pName)
|
||||||
|
t.Failed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestParseParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
Error error
|
||||||
|
ErrorAlternative error
|
||||||
|
}{
|
||||||
|
{ // invalid param name prefix
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"_param1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {_param1}")),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // invalid param name suffix
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1_": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {param1_}")),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // missing param description
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // empty param description
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {
|
||||||
|
"info": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // missing param type
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {
|
||||||
|
"info": "valid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // empty param type
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {
|
||||||
|
"info": "valid",
|
||||||
|
"type": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // invalid type (optional mark only)
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {
|
||||||
|
"info": "valid",
|
||||||
|
"type": "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
|
||||||
|
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
||||||
|
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
||||||
|
},
|
||||||
|
{ // valid description + valid type
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {
|
||||||
|
"info": "valid",
|
||||||
|
"type": "a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // valid description + valid OPTIONAL type
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": {
|
||||||
|
"info": "valid",
|
||||||
|
"type": "?valid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // name conflict with rename
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid" },
|
||||||
|
"param2": { "info": "valid", "type": "valid", "name": "param1" }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
// 2 possible errors as map order is not deterministic
|
||||||
|
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")),
|
||||||
|
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")),
|
||||||
|
},
|
||||||
|
{ // rename conflict with name
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid", "name": "param2" },
|
||||||
|
"param2": { "info": "valid", "type": "valid" }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
// 2 possible errors as map order is not deterministic
|
||||||
|
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")),
|
||||||
|
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")),
|
||||||
|
},
|
||||||
|
{ // rename conflict with rename
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid", "name": "conflict" },
|
||||||
|
"param2": { "info": "valid", "type": "valid", "name": "conflict" }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
// 2 possible errors as map order is not deterministic
|
||||||
|
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")),
|
||||||
|
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")),
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // both renamed with no conflict
|
||||||
|
`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid", "name": "freename" },
|
||||||
|
"param2": { "info": "valid", "type": "valid", "name": "freename2" }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if err == nil && test.Error != nil {
|
||||||
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
if err != nil && test.Error == nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && test.Error != nil {
|
||||||
|
if err.Error() != test.Error.Error() && err.Error() != test.ErrorAlternative.Error() {
|
||||||
|
t.Errorf("got the error: '%s'", err.Error())
|
||||||
|
t.Errorf("expected error (alternative 1): '%s'", test.Error.Error())
|
||||||
|
t.Errorf("expected error (alternative 2): '%s'", test.ErrorAlternative.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBrowseSimple(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
Path []string
|
||||||
|
BrowseDepth int
|
||||||
|
ValidDepth bool
|
||||||
|
}{
|
||||||
|
{ // false positive -1
|
||||||
|
`{
|
||||||
|
"/" : {
|
||||||
|
"parent": {
|
||||||
|
"/": {
|
||||||
|
"subdir": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[]string{"parent", "subdir"},
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // false positive +1
|
||||||
|
`{
|
||||||
|
"/" : {
|
||||||
|
"parent": {
|
||||||
|
"/": {
|
||||||
|
"subdir": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[]string{"parent", "subdir"},
|
||||||
|
3,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"/" : {
|
||||||
|
"parent": {
|
||||||
|
"/": {
|
||||||
|
"subdir": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[]string{"parent", "subdir"},
|
||||||
|
2,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // unknown path
|
||||||
|
`{
|
||||||
|
"/" : {
|
||||||
|
"parent": {
|
||||||
|
"/": {
|
||||||
|
"subdir": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[]string{"x", "y"},
|
||||||
|
2,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // unknown path
|
||||||
|
`{
|
||||||
|
"/" : {
|
||||||
|
"parent": {
|
||||||
|
"/": {
|
||||||
|
"subdir": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[]string{"parent", "y"},
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // Warning: this case is important to understand the precedence of service paths over
|
||||||
|
// the value of some variables. Here if we send a string parameter in the GET method that
|
||||||
|
// unfortunately is equal to 'subdir', it will call the sub-service /parent/subdir' instead
|
||||||
|
// of the service /parent with its parameter set to the value 'subdir'.
|
||||||
|
`{
|
||||||
|
"/" : {
|
||||||
|
"parent": {
|
||||||
|
"/": {
|
||||||
|
"subdir": {}
|
||||||
|
},
|
||||||
|
"GET": {
|
||||||
|
"info": "valid-desc",
|
||||||
|
"in": {
|
||||||
|
"some-value": {
|
||||||
|
"info": "valid-desc",
|
||||||
|
"type": "valid-type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[]string{"parent", "subdir"},
|
||||||
|
2,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
|
srv, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, depth := srv.Browse(test.Path)
|
||||||
|
if test.ValidDepth {
|
||||||
|
if depth != test.BrowseDepth {
|
||||||
|
t.Errorf("expected a depth of %d (got %d)", test.BrowseDepth, depth)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if depth == test.BrowseDepth {
|
||||||
|
t.Errorf("expected a depth NOT %d (got %d)", test.BrowseDepth, depth)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "git.xdrm.io/go/aicra/internal/cerr"
|
||||||
|
|
||||||
|
// ErrRead - a problem ocurred when trying to read the configuration file
|
||||||
|
const ErrRead = cerr.Error("cannot read config")
|
||||||
|
|
||||||
|
// ErrFormat - a invalid format has been detected
|
||||||
|
const ErrFormat = cerr.Error("invalid config format")
|
||||||
|
|
||||||
|
// ErrIllegalServiceName - an illegal character has been found in a service name
|
||||||
|
const ErrIllegalServiceName = cerr.Error("service must not contain any slash '/' nor '-' symbols")
|
||||||
|
|
||||||
|
// ErrMissingMethodDesc - a method is missing its description
|
||||||
|
const ErrMissingMethodDesc = cerr.Error("missing method description")
|
||||||
|
|
||||||
|
// ErrMissingParamDesc - a parameter is missing its description
|
||||||
|
const ErrMissingParamDesc = cerr.Error("missing parameter description")
|
||||||
|
|
||||||
|
// ErrIllegalParamName - a parameter has an illegal name
|
||||||
|
const ErrIllegalParamName = cerr.Error("illegal parameter name (must not begin/end with '_')")
|
||||||
|
|
||||||
|
// ErrMissingParamType - a parameter has an illegal type
|
||||||
|
const ErrMissingParamType = cerr.Error("missing parameter type")
|
||||||
|
|
||||||
|
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
|
||||||
|
const ErrParamNameConflict = cerr.Error("name conflict for parameter")
|
|
@ -1,7 +1,6 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,7 +9,7 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e
|
||||||
|
|
||||||
// 1. fail on missing description
|
// 1. fail on missing description
|
||||||
if len(methodDef.Description) < 1 {
|
if len(methodDef.Description) < 1 {
|
||||||
return fmt.Errorf("missing %s.%s description", servicePath, httpMethod)
|
return ErrMissingMethodDesc.WrapString(httpMethod + " " + servicePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. stop if no parameter
|
// 2. stop if no parameter
|
||||||
|
@ -22,16 +21,16 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e
|
||||||
// 3. for each parameter
|
// 3. for each parameter
|
||||||
for pName, pData := range methodDef.Parameters {
|
for pName, pData := range methodDef.Parameters {
|
||||||
|
|
||||||
// check name
|
// 3.1. check name
|
||||||
if strings.Trim(pName, "_") != pName {
|
if strings.Trim(pName, "_") != pName {
|
||||||
return fmt.Errorf("invalid name '%s' must not begin/end with '_'", pName)
|
return ErrIllegalParamName.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pData.Rename) < 1 {
|
if len(pData.Rename) < 1 {
|
||||||
pData.Rename = pName
|
pData.Rename = pName
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Check for name/rename conflict
|
// 3.2. Check for name/rename conflict
|
||||||
for paramName, param := range methodDef.Parameters {
|
for paramName, param := range methodDef.Parameters {
|
||||||
|
|
||||||
// ignore self
|
// ignore self
|
||||||
|
@ -39,39 +38,26 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Same rename field
|
// 3.2.1. Same rename field
|
||||||
if pData.Rename == param.Rename {
|
// 3.2.2. Not-renamed field matches a renamed field
|
||||||
return fmt.Errorf("rename conflict for %s.%s parameter '%s'", servicePath, httpMethod, pData.Rename)
|
// 3.2.3. Renamed field matches name
|
||||||
}
|
if pData.Rename == param.Rename || pName == param.Rename || pData.Rename == paramName {
|
||||||
|
return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
||||||
// 2. Not-renamed field matches a renamed field
|
|
||||||
if pName == param.Rename {
|
|
||||||
return fmt.Errorf("name conflict for %s.%s parameter '%s'", servicePath, httpMethod, pName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Renamed field matches name
|
|
||||||
if pData.Rename == paramName {
|
|
||||||
return fmt.Errorf("name conflict for %s.%s parameter '%s'", servicePath, httpMethod, pName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Manage invalid type
|
// 3.3. Fail on missing description
|
||||||
if len(pData.Type) < 1 {
|
|
||||||
return fmt.Errorf("invalid type for %s.%s parameter '%s'", servicePath, httpMethod, pName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Fail on missing description
|
|
||||||
if len(pData.Description) < 1 {
|
if len(pData.Description) < 1 {
|
||||||
return fmt.Errorf("missing description for %s.%s parameter '%s'", servicePath, httpMethod, pName)
|
return ErrMissingParamDesc.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Fail on missing type
|
// 3.4. Manage invalid type
|
||||||
if len(pData.Type) < 1 {
|
if len(pData.Type) < 1 || pData.Type == "?" {
|
||||||
return fmt.Errorf("missing type for %s.%s parameter '%s'", servicePath, httpMethod, pName)
|
return ErrMissingParamType.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Set optional + type
|
// 3.5. Set optional + type
|
||||||
if pData.Type[0] == '?' {
|
if pData.Type[0] == '?' {
|
||||||
pData.Optional = true
|
pData.Optional = true
|
||||||
pData.Type = pData.Type[1:]
|
pData.Type = pData.Type[1:]
|
||||||
|
@ -81,13 +67,3 @@ func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) e
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// scopeHasPermission returns whether the permission fulfills a given scope
|
|
||||||
func scopeHasPermission(permission string, scope []string) bool {
|
|
||||||
for _, s := range scope {
|
|
||||||
if permission == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,20 +2,11 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/cerr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrRead - a problem ocurred when trying to read the configuration file
|
|
||||||
const ErrRead = cerr.Error("cannot read config")
|
|
||||||
|
|
||||||
// ErrFormat - a invalid format has been detected
|
|
||||||
const ErrFormat = cerr.Error("invalid config format")
|
|
||||||
|
|
||||||
// Parse builds a service from a json reader and checks for most format errors.
|
// Parse builds a service from a json reader and checks for most format errors.
|
||||||
func Parse(r io.Reader) (*Service, error) {
|
func Parse(r io.Reader) (*Service, error) {
|
||||||
receiver := &Service{}
|
receiver := &Service{}
|
||||||
|
@ -87,20 +78,20 @@ func (svc *Service) checkAndFormat(servicePath string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. stop if no child */
|
// 2. stop if no child */
|
||||||
if svc.Children == nil || len(svc.Children) < 1 {
|
if svc.Children == nil || len(svc.Children) < 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. for each service */
|
// 3. for each service */
|
||||||
for childService, ctl := range svc.Children {
|
for childService, ctl := range svc.Children {
|
||||||
|
|
||||||
// 3. invalid name */
|
// 3.1. invalid name */
|
||||||
if strings.ContainsAny(childService, "/-") {
|
if strings.ContainsAny(childService, "/-") {
|
||||||
return fmt.Errorf("service '%s' must not contain any slash '/' nor '-' symbols", childService)
|
return ErrIllegalServiceName.WrapString(childService)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. check recursively */
|
// 3.2. check recursively */
|
||||||
err := ctl.checkAndFormat(childService)
|
err := ctl.checkAndFormat(childService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
package multipart
|
package multipart
|
||||||
|
|
||||||
// ConstError is a wrapper to set constant errors
|
import "git.xdrm.io/go/aicra/internal/cerr"
|
||||||
type ConstError string
|
|
||||||
|
|
||||||
// Error implements error
|
|
||||||
func (err ConstError) Error() string {
|
|
||||||
return string(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
||||||
const ErrMissingDataName = ConstError("data has no name")
|
const ErrMissingDataName = cerr.Error("data has no name")
|
||||||
|
|
||||||
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
||||||
const ErrDataNameConflict = ConstError("data name conflict")
|
const ErrDataNameConflict = cerr.Error("data name conflict")
|
||||||
|
|
||||||
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
||||||
const ErrNoHeader = ConstError("data has no header")
|
const ErrNoHeader = cerr.Error("data has no header")
|
||||||
|
|
||||||
// Component represents a multipart variable/file
|
// Component represents a multipart variable/file
|
||||||
type Component struct {
|
type Component struct {
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
package reqdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseParameter parses http GET/POST data
|
|
||||||
// - []string
|
|
||||||
// - size = 1 : return json of first element
|
|
||||||
// - size > 1 : return array of json elements
|
|
||||||
// - string : return json if valid, else return raw string
|
|
||||||
func parseParameter(data interface{}) interface{} {
|
|
||||||
dtype := reflect.TypeOf(data)
|
|
||||||
dvalue := reflect.ValueOf(data)
|
|
||||||
|
|
||||||
switch dtype.Kind() {
|
|
||||||
|
|
||||||
/* (1) []string -> recursive */
|
|
||||||
case reflect.Slice:
|
|
||||||
|
|
||||||
// 1. Return nothing if empty
|
|
||||||
if dvalue.Len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. only return first element if alone
|
|
||||||
if dvalue.Len() == 1 {
|
|
||||||
|
|
||||||
element := dvalue.Index(0)
|
|
||||||
if element.Kind() != reflect.String {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return parseParameter(element.String())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Return all elements if more than 1
|
|
||||||
result := make([]interface{}, dvalue.Len())
|
|
||||||
|
|
||||||
for i, l := 0, dvalue.Len(); i < l; i++ {
|
|
||||||
element := dvalue.Index(i)
|
|
||||||
|
|
||||||
// ignore non-string
|
|
||||||
if element.Kind() != reflect.String {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result[i] = parseParameter(element.String())
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
/* (2) string -> parse */
|
|
||||||
case reflect.String:
|
|
||||||
|
|
||||||
// build json wrapper
|
|
||||||
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String())
|
|
||||||
|
|
||||||
// try to parse as json
|
|
||||||
var result interface{}
|
|
||||||
err := json.Unmarshal([]byte(wrapper), &result)
|
|
||||||
|
|
||||||
// return if success
|
|
||||||
if err == nil {
|
|
||||||
|
|
||||||
mapval, ok := result.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return dvalue.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapped, ok := mapval["wrapped"]
|
|
||||||
if !ok {
|
|
||||||
return dvalue.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapped
|
|
||||||
}
|
|
||||||
|
|
||||||
// else return as string
|
|
||||||
return dvalue.String()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (3) NIL if unknown type */
|
|
||||||
return dvalue
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +1,22 @@
|
||||||
package reqdata
|
package reqdata
|
||||||
|
|
||||||
|
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
|
// Parameter represents an http request parameter
|
||||||
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
||||||
type Parameter struct {
|
type Parameter struct {
|
||||||
|
@ -16,14 +33,96 @@ type Parameter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parameter (json-like) if not already done
|
// Parse parameter (json-like) if not already done
|
||||||
func (i *Parameter) Parse() {
|
func (i *Parameter) Parse() error {
|
||||||
|
|
||||||
/* (1) Stop if already parsed or nil*/
|
/* (1) Stop if already parsed or nil*/
|
||||||
if i.Parsed || i.Value == nil {
|
if i.Parsed || i.Value == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (2) Try to parse value */
|
/* (2) Try to parse value */
|
||||||
i.Value = parseParameter(i.Value)
|
parsed, err := parseParameter(i.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Parsed = true
|
||||||
|
i.Value = parsed
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParameter parses http GET/POST data
|
||||||
|
// - []string
|
||||||
|
// - size = 1 : return json of first element
|
||||||
|
// - size > 1 : return array of json elements
|
||||||
|
// - string : return json if valid, else return raw string
|
||||||
|
func parseParameter(data interface{}) (interface{}, error) {
|
||||||
|
dtype := reflect.TypeOf(data)
|
||||||
|
dvalue := reflect.ValueOf(data)
|
||||||
|
|
||||||
|
switch dtype.Kind() {
|
||||||
|
|
||||||
|
/* (1) []string -> recursive */
|
||||||
|
case reflect.Slice:
|
||||||
|
|
||||||
|
// 1. ignore empty
|
||||||
|
if dvalue.Len() == 0 {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. parse each element recursively
|
||||||
|
result := make([]interface{}, dvalue.Len())
|
||||||
|
|
||||||
|
for i, l := 0, dvalue.Len(); i < l; i++ {
|
||||||
|
element := dvalue.Index(i)
|
||||||
|
|
||||||
|
// ignore non-string
|
||||||
|
if element.Kind() != reflect.String {
|
||||||
|
result[i] = element.Interface()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := parseParameter(element.String())
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
result[i] = parsed
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
/* (2) string -> parse */
|
||||||
|
case reflect.String:
|
||||||
|
|
||||||
|
// build json wrapper
|
||||||
|
wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String())
|
||||||
|
|
||||||
|
// try to parse as json
|
||||||
|
var result interface{}
|
||||||
|
err := json.Unmarshal([]byte(wrapper), &result)
|
||||||
|
|
||||||
|
// return if success
|
||||||
|
if err == nil {
|
||||||
|
|
||||||
|
mapval, ok := result.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return dvalue.String(), ErrInvalidRootType
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped, ok := mapval["wrapped"]
|
||||||
|
if !ok {
|
||||||
|
return dvalue.String(), ErrInvalidJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// else return as string
|
||||||
|
return dvalue.String(), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (3) NIL if unknown type */
|
||||||
|
return dvalue.Interface(), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,358 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimpleString(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: "some-string"}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := p.Value.(string)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a string")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cast != "some-string" {
|
||||||
|
t.Errorf("expected parameter to equal 'some-string', got '%s'", cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleFloat(t *testing.T) {
|
||||||
|
tcases := []float64{12.3456789, -12.3456789, 0.0000001, -0.0000001}
|
||||||
|
|
||||||
|
for i, tcase := range tcases {
|
||||||
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: tcase}
|
||||||
|
|
||||||
|
if err := p.Parse(); err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := p.Value.(float64)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a float64")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if math.Abs(cast-tcase) > 0.00000001 {
|
||||||
|
t.Errorf("expected parameter to equal '%f', got '%f'", tcase, cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleBool(t *testing.T) {
|
||||||
|
tcases := []bool{true, false}
|
||||||
|
|
||||||
|
for i, tcase := range tcases {
|
||||||
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: tcase}
|
||||||
|
|
||||||
|
if err := p.Parse(); err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := p.Value.(bool)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a bool")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cast != tcase {
|
||||||
|
t.Errorf("expected parameter to equal '%t', got '%t'", tcase, cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonStringSlice(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: `["str1", "str2"]`}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, canCast := p.Value.([]interface{})
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(slice) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %d", len(slice))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []string{"str1", "str2"}
|
||||||
|
|
||||||
|
for i, res := range results {
|
||||||
|
|
||||||
|
cast, canCast := slice[i].(string)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter %d to be a []string", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cast != res {
|
||||||
|
t.Errorf("expected first value to be '%s', got '%s'", res, cast)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringSlice(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: []string{"str1", "str2"}}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, canCast := p.Value.([]interface{})
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(slice) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %d", len(slice))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []string{"str1", "str2"}
|
||||||
|
|
||||||
|
for i, res := range results {
|
||||||
|
|
||||||
|
cast, canCast := slice[i].(string)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter %d to be a []string", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cast != res {
|
||||||
|
t.Errorf("expected first value to be '%s', got '%s'", res, cast)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonPrimitiveBool(t *testing.T) {
|
||||||
|
tcases := []struct {
|
||||||
|
Raw string
|
||||||
|
BoolValue bool
|
||||||
|
}{
|
||||||
|
{"true", true},
|
||||||
|
{"false", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tcase := range tcases {
|
||||||
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: tcase.Raw}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := p.Value.(bool)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a bool")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cast != tcase.BoolValue {
|
||||||
|
t.Errorf("expected a value of %t, got %t", tcase.BoolValue, cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonPrimitiveFloat(t *testing.T) {
|
||||||
|
tcases := []struct {
|
||||||
|
Raw string
|
||||||
|
FloatValue float64
|
||||||
|
}{
|
||||||
|
{"1", 1},
|
||||||
|
{"-1", -1},
|
||||||
|
|
||||||
|
{"0.001", 0.001},
|
||||||
|
{"-0.001", -0.001},
|
||||||
|
|
||||||
|
{"1.9992", 1.9992},
|
||||||
|
{"-1.9992", -1.9992},
|
||||||
|
|
||||||
|
{"19992", 19992},
|
||||||
|
{"-19992", -19992},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tcase := range tcases {
|
||||||
|
t.Run("case "+string(i), func(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: tcase.Raw}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := p.Value.(float64)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a float64")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if math.Abs(cast-tcase.FloatValue) > 0.00001 {
|
||||||
|
t.Errorf("expected a value of %f, got %f", tcase.FloatValue, cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonBoolSlice(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: []string{"true", "false"}}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, canCast := p.Value.([]interface{})
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(slice) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %d", len(slice))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []bool{true, false}
|
||||||
|
|
||||||
|
for i, res := range results {
|
||||||
|
|
||||||
|
cast, canCast := slice[i].(bool)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter %d to be a []bool", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cast != res {
|
||||||
|
t.Errorf("expected first value to be '%t', got '%t'", res, cast)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolSlice(t *testing.T) {
|
||||||
|
p := Parameter{Parsed: false, File: false, Value: []bool{true, false}}
|
||||||
|
|
||||||
|
err := p.Parse()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.Parsed {
|
||||||
|
t.Errorf("expected parameter to be parsed")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, canCast := p.Value.([]interface{})
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter to be a []interface{}")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(slice) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %d", len(slice))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []bool{true, false}
|
||||||
|
|
||||||
|
for i, res := range results {
|
||||||
|
|
||||||
|
cast, canCast := slice[i].(bool)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("expected parameter %d to be a bool, got %v", i, slice[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cast != res {
|
||||||
|
t.Errorf("expected first value to be '%t', got '%t'", res, cast)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -57,6 +57,11 @@ func New(uriParams []string, req *http.Request) *Store {
|
||||||
// 1. set URI parameters
|
// 1. set URI parameters
|
||||||
ds.setURIParams(uriParams)
|
ds.setURIParams(uriParams)
|
||||||
|
|
||||||
|
// ignore nil requests
|
||||||
|
if req == nil {
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
// 2. GET (query) data
|
// 2. GET (query) data
|
||||||
ds.readQuery(req)
|
ds.readQuery(req)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,804 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
server.go
14
server.go
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
// Server represents an AICRA instance featuring: type checkers, services
|
// Server represents an AICRA instance featuring: type checkers, services
|
||||||
type Server struct {
|
type Server struct {
|
||||||
services *config.Service
|
config *config.Service
|
||||||
Checkers *checker.Set
|
Checkers *checker.Set
|
||||||
handlers []*api.Handler
|
handlers []*api.Handler
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func New(configPath string) (*Server, error) {
|
||||||
|
|
||||||
// 1. init instance
|
// 1. init instance
|
||||||
var i = &Server{
|
var i = &Server{
|
||||||
services: nil,
|
config: nil,
|
||||||
Checkers: checker.New(),
|
Checkers: checker.New(),
|
||||||
handlers: make([]*api.Handler, 0),
|
handlers: make([]*api.Handler, 0),
|
||||||
}
|
}
|
||||||
|
@ -40,14 +40,14 @@ func New(configPath string) (*Server, error) {
|
||||||
defer configFile.Close()
|
defer configFile.Close()
|
||||||
|
|
||||||
// 3. load configuration
|
// 3. load configuration
|
||||||
i.services, err = config.Parse(configFile)
|
i.config, err = config.Parse(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. log configuration services
|
// 4. log configuration services
|
||||||
log.Printf("=== Aicra configuration ===\n")
|
log.Printf("🔧 Reading configuration '%s'\n", configPath)
|
||||||
logService(*i.services, "")
|
logService(*i.config, "")
|
||||||
|
|
||||||
return i, nil
|
return i, nil
|
||||||
|
|
||||||
|
@ -68,9 +68,9 @@ func (s *Server) Handle(handler *api.Handler) {
|
||||||
func (s Server) HTTP() httpServer {
|
func (s Server) HTTP() httpServer {
|
||||||
|
|
||||||
// 1. log available handlers
|
// 1. log available handlers
|
||||||
log.Printf("=== Mapped handlers ===\n")
|
log.Printf("🔗 Mapping handlers\n")
|
||||||
for i := 0; i < len(s.handlers); i++ {
|
for i := 0; i < len(s.handlers); i++ {
|
||||||
log.Printf("* [rest] %s\t'%s'\n", s.handlers[i].GetMethod(), s.handlers[i].GetPath())
|
log.Printf(" ->\t%s\t'%s'\n", s.handlers[i].GetMethod(), s.handlers[i].GetPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. cast to http server
|
// 2. cast to http server
|
||||||
|
|
|
@ -41,6 +41,18 @@ func TestString_AvailableTypes(t *testing.T) {
|
||||||
{"string(1 )", false},
|
{"string(1 )", false},
|
||||||
{"string( 1 )", false},
|
{"string( 1 )", false},
|
||||||
|
|
||||||
|
{"string()", false},
|
||||||
|
{"string(a)", false},
|
||||||
|
{"string(-1)", false},
|
||||||
|
|
||||||
|
{"string(,)", false},
|
||||||
|
{"string(1,b)", false},
|
||||||
|
{"string(a,b)", false},
|
||||||
|
{"string(a,1)", false},
|
||||||
|
{"string(-1,1)", false},
|
||||||
|
{"string(1,-1)", false},
|
||||||
|
{"string(-1,-1)", false},
|
||||||
|
|
||||||
{"string(1,2)", true},
|
{"string(1,2)", true},
|
||||||
{"string(1, 2)", true},
|
{"string(1, 2)", true},
|
||||||
{"string(1, 2)", false},
|
{"string(1, 2)", false},
|
||||||
|
|
|
@ -96,6 +96,11 @@ func TestUint_Values(t *testing.T) {
|
||||||
// strane offset because of how precision works
|
// strane offset because of how precision works
|
||||||
{fmt.Sprintf("%f", float64(math.MaxUint64+1024*3)), false},
|
{fmt.Sprintf("%f", float64(math.MaxUint64+1024*3)), false},
|
||||||
|
|
||||||
|
{[]byte(fmt.Sprintf("%d", math.MaxInt64)), true},
|
||||||
|
{[]byte(fmt.Sprintf("%d", uint(math.MaxUint64))), true},
|
||||||
|
// strane offset because of how precision works
|
||||||
|
{[]byte(fmt.Sprintf("%f", float64(math.MaxUint64+1024*3))), false},
|
||||||
|
|
||||||
{"string", false},
|
{"string", false},
|
||||||
{[]byte("bytes"), false},
|
{[]byte("bytes"), false},
|
||||||
{-0.1, false},
|
{-0.1, false},
|
||||||
|
|
6
util.go
6
util.go
|
@ -58,7 +58,7 @@ func (s *httpServer) extractParameters(store *reqdata.Store, methodParam map[str
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. do not check if file
|
// 6. ignore type check if file
|
||||||
if gotFile {
|
if gotFile {
|
||||||
parameters[param.Rename] = p.Value
|
parameters[param.Rename] = p.Value
|
||||||
continue
|
continue
|
||||||
|
@ -90,9 +90,9 @@ func logService(s config.Service, path string) {
|
||||||
for _, method := range handledMethods {
|
for _, method := range handledMethods {
|
||||||
if m := s.Method(method); m != nil {
|
if m := s.Method(method); m != nil {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
log.Printf("* [rest] %s\t'/'\n", method)
|
log.Printf(" ->\t%s\t'/'\n", method)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("* [rest] %s\t'%s'\n", method, path)
|
log.Printf(" ->\t%s\t'%s'\n", method, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue