diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8af74d15..36109e6e 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,10 +1,23 @@ { "ImportPath": "github.com/gin-gonic/gin", - "GoVersion": "go1.3", + "GoVersion": "go1.4.2", "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "b428fda53bb0a764fea9c76c9413512eda291dec" + "Rev": "999ba04938b528fb4fb859231ee929958b8db4a6" + }, + { + "ImportPath": "github.com/mattn/go-colorable", + "Rev": "043ae16291351db8465272edf465c9f388161627" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "de7fcff264cd05cc0c90c509ea789a436a0dd206" + }, + { + "ImportPath": "gopkg.in/joeybloggs/go-validate-yourself.v4", + "Comment": "v4.0", + "Rev": "a3cb430fa1e43b15e72d7bec5b20d0bdff4c2bb8" } ] } diff --git a/auth.go b/auth.go index 9caf072e..0cf64e59 100644 --- a/auth.go +++ b/auth.go @@ -33,10 +33,7 @@ func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } // the key is the user name and the value is the password, as well as the name of the Realm // (see http://tools.ietf.org/html/rfc2617#section-1.2) func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { - pairs, err := processAccounts(accounts) - if err != nil { - panic(err) - } + pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) @@ -61,14 +58,14 @@ func BasicAuth(accounts Accounts) HandlerFunc { return BasicAuthForRealm(accounts, "") } -func processAccounts(accounts Accounts) (authPairs, error) { +func processAccounts(accounts Accounts) authPairs { if len(accounts) == 0 { - return nil, errors.New("Empty list of authorized credentials") + panic("Empty list of authorized credentials") } pairs := make(authPairs, 0, len(accounts)) for user, password := range accounts { if len(user) == 0 { - return nil, errors.New("User can not be empty") + panic("User can not be empty") } base := user + ":" + password value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) @@ -79,7 +76,7 @@ func processAccounts(accounts Accounts) (authPairs, error) { } // We have to sort the credentials in order to use bsearch later. sort.Sort(pairs) - return pairs, nil + return pairs } func searchCredential(pairs authPairs, auth string) (string, bool) { diff --git a/auth_test.go b/auth_test.go index 067dfb19..d2f165cd 100644 --- a/auth_test.go +++ b/auth_test.go @@ -27,7 +27,7 @@ func TestBasicAuthSucceed(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be Ok, was: %d", w.Code) } bodyAsString := w.Body.String() @@ -52,7 +52,7 @@ func TestBasicAuth401(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %s", w.Code) + t.Errorf("Response code should be Not autorized, was: %d", w.Code) } if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" { @@ -76,7 +76,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %s", w.Code) + t.Errorf("Response code should be Not autorized, was: %d", w.Code) } if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" { diff --git a/binding/binding.go b/binding/binding.go index 752c9129..26babeb7 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -5,280 +5,48 @@ package binding import ( - "encoding/json" - "encoding/xml" - "errors" "net/http" - "reflect" - "strconv" - "strings" + + "gopkg.in/joeybloggs/go-validate-yourself.v4" ) -type ( - Binding interface { - Bind(*http.Request, interface{}) error - } - - // JSON binding - jsonBinding struct{} - - // XML binding - xmlBinding struct{} - - // form binding - formBinding struct{} - - // multipart form binding - multipartFormBinding struct{} +const ( + MIMEJSON = "application/json" + MIMEHTML = "text/html" + MIMEXML = "application/xml" + MIMEXML2 = "text/xml" + MIMEPlain = "text/plain" + MIMEPOSTForm = "application/x-www-form-urlencoded" + MIMEMultipartPOSTForm = "multipart/form-data" ) -const MAX_MEMORY = 1 * 1024 * 1024 +type Binding interface { + Name() string + Bind(*http.Request, interface{}) error +} + +var _validator = validator.NewValidator("binding", validator.BakedInValidators) var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} // todo - MultipartForm = multipartFormBinding{} + JSON = jsonBinding{} + XML = xmlBinding{} + GETForm = getFormBinding{} + POSTForm = postFormBinding{} ) -func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { - decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) +func Default(method, contentType string) Binding { + if method == "GET" { + return GETForm } else { - return err - } -} - -func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { - decoder := xml.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) - } else { - return err - } -} - -func (_ formBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - if err := mapForm(obj, req.Form); err != nil { - return err - } - return Validate(obj) -} - -func (_ multipartFormBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseMultipartForm(MAX_MEMORY); err != nil { - return err - } - if err := mapForm(obj, req.Form); err != nil { - return err - } - return Validate(obj) -} - -func mapForm(ptr interface{}, form map[string][]string) error { - typ := reflect.TypeOf(ptr).Elem() - formStruct := reflect.ValueOf(ptr).Elem() - for i := 0; i < typ.NumField(); i++ { - typeField := typ.Field(i) - if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" { - structField := formStruct.Field(i) - if !structField.CanSet() { - continue - } - - inputValue, exists := form[inputFieldName] - if !exists { - continue - } - numElems := len(inputValue) - if structField.Kind() == reflect.Slice && numElems > 0 { - sliceOf := structField.Type().Elem().Kind() - slice := reflect.MakeSlice(structField.Type(), numElems, numElems) - for i := 0; i < numElems; i++ { - if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { - return err - } - } - formStruct.Field(i).Set(slice) - } else { - if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { - return err - } - } + switch contentType { + case MIMEPOSTForm: + return POSTForm + case MIMEJSON: + return JSON + case MIMEXML, MIMEXML2: + return XML + default: + return GETForm } } - return nil -} - -func setIntField(val string, bitSize int, structField reflect.Value) error { - if val == "" { - val = "0" - } - - intVal, err := strconv.ParseInt(val, 10, bitSize) - if err == nil { - structField.SetInt(intVal) - } - - return err -} - -func setUintField(val string, bitSize int, structField reflect.Value) error { - if val == "" { - val = "0" - } - - uintVal, err := strconv.ParseUint(val, 10, bitSize) - if err == nil { - structField.SetUint(uintVal) - } - - return err -} - -func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { - switch valueKind { - case reflect.Int: - return setIntField(val, 0, structField) - case reflect.Int8: - return setIntField(val, 8, structField) - case reflect.Int16: - return setIntField(val, 16, structField) - case reflect.Int32: - return setIntField(val, 32, structField) - case reflect.Int64: - return setIntField(val, 64, structField) - case reflect.Uint: - return setUintField(val, 0, structField) - case reflect.Uint8: - return setUintField(val, 8, structField) - case reflect.Uint16: - return setUintField(val, 16, structField) - case reflect.Uint32: - return setUintField(val, 32, structField) - case reflect.Uint64: - return setUintField(val, 64, structField) - case reflect.Bool: - if val == "" { - val = "false" - } - boolVal, err := strconv.ParseBool(val) - if err != nil { - return err - } else { - structField.SetBool(boolVal) - } - case reflect.Float32: - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, 32) - if err != nil { - return err - } else { - structField.SetFloat(floatVal) - } - case reflect.Float64: - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, 64) - if err != nil { - return err - } else { - structField.SetFloat(floatVal) - } - case reflect.String: - structField.SetString(val) - } - return nil -} - -// Don't pass in pointers to bind to. Can lead to bugs. See: -// https://github.com/codegangsta/martini-contrib/issues/40 -// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 -func ensureNotPointer(obj interface{}) { - if reflect.TypeOf(obj).Kind() == reflect.Ptr { - panic("Pointers are not accepted as binding models") - } -} - -func Validate(obj interface{}, parents ...string) error { - typ := reflect.TypeOf(obj) - val := reflect.ValueOf(obj) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - switch typ.Kind() { - case reflect.Struct: - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - - // Allow ignored and unexported fields in the struct - if len(field.PkgPath) > 0 || field.Tag.Get("form") == "-" { - continue - } - - fieldValue := val.Field(i).Interface() - zero := reflect.Zero(field.Type).Interface() - - if strings.Index(field.Tag.Get("binding"), "required") > -1 { - fieldType := field.Type.Kind() - if fieldType == reflect.Struct { - if reflect.DeepEqual(zero, fieldValue) { - return errors.New("Required " + field.Name) - } - err := Validate(fieldValue, field.Name) - if err != nil { - return err - } - } else if reflect.DeepEqual(zero, fieldValue) { - if len(parents) > 0 { - return errors.New("Required " + field.Name + " on " + parents[0]) - } else { - return errors.New("Required " + field.Name) - } - } else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct { - err := Validate(fieldValue) - if err != nil { - return err - } - } - } else { - fieldType := field.Type.Kind() - if fieldType == reflect.Struct { - if reflect.DeepEqual(zero, fieldValue) { - continue - } - err := Validate(fieldValue, field.Name) - if err != nil { - return err - } - } else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct { - err := Validate(fieldValue, field.Name) - if err != nil { - return err - } - } - } - } - case reflect.Slice: - for i := 0; i < val.Len(); i++ { - fieldValue := val.Index(i).Interface() - err := Validate(fieldValue) - if err != nil { - return err - } - } - default: - return nil - } - return nil } diff --git a/binding/form_mapping.go b/binding/form_mapping.go new file mode 100644 index 00000000..d359998c --- /dev/null +++ b/binding/form_mapping.go @@ -0,0 +1,140 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "reflect" + "strconv" +) + +func mapForm(ptr interface{}, form map[string][]string) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + + inputFieldName := typeField.Tag.Get("form") + if inputFieldName == "" { + inputFieldName = typeField.Name + } + inputValue, exists := form[inputFieldName] + + if !exists { + continue + } + + numElems := len(inputValue) + if structField.Kind() == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { + return err + } + } + val.Field(i).Set(slice) + } else { + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + } + } + + } + return nil +} + +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + switch valueKind { + case reflect.Int: + return setIntField(val, 0, structField) + case reflect.Int8: + return setIntField(val, 8, structField) + case reflect.Int16: + return setIntField(val, 16, structField) + case reflect.Int32: + return setIntField(val, 32, structField) + case reflect.Int64: + return setIntField(val, 64, structField) + case reflect.Uint: + return setUintField(val, 0, structField) + case reflect.Uint8: + return setUintField(val, 8, structField) + case reflect.Uint16: + return setUintField(val, 16, structField) + case reflect.Uint32: + return setUintField(val, 32, structField) + case reflect.Uint64: + return setUintField(val, 64, structField) + case reflect.Bool: + return setBoolField(val, structField) + case reflect.Float32: + return setFloatField(val, 32, structField) + case reflect.Float64: + return setFloatField(val, 64, structField) + case reflect.String: + structField.SetString(val) + default: + return errors.New("Unknown type") + } + return nil +} + +func setIntField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, bitSize) + if err == nil { + field.SetInt(intVal) + } + return err +} + +func setUintField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, bitSize) + if err == nil { + field.SetUint(uintVal) + } + return err +} + +func setBoolField(val string, field reflect.Value) error { + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err == nil { + field.SetBool(boolVal) + } + return nil +} + +func setFloatField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, bitSize) + if err == nil { + field.SetFloat(floatVal) + } + return err +} + +// Don't pass in pointers to bind to. Can lead to bugs. See: +// https://github.com/codegangsta/martini-contrib/issues/40 +// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 +func ensureNotPointer(obj interface{}) { + if reflect.TypeOf(obj).Kind() == reflect.Ptr { + panic("Pointers are not accepted as binding models") + } +} diff --git a/binding/get_form.go b/binding/get_form.go new file mode 100644 index 00000000..a1717886 --- /dev/null +++ b/binding/get_form.go @@ -0,0 +1,26 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import "net/http" + +type getFormBinding struct{} + +func (_ getFormBinding) Name() string { + return "get_form" +} + +func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := mapForm(obj, req.Form); err != nil { + return err + } + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/binding/json.go b/binding/json.go new file mode 100644 index 00000000..1f38618a --- /dev/null +++ b/binding/json.go @@ -0,0 +1,28 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "encoding/json" + + "net/http" +) + +type jsonBinding struct{} + +func (_ jsonBinding) Name() string { + return "json" +} + +func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(obj); err != nil { + return err + } + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/binding/post_form.go b/binding/post_form.go new file mode 100644 index 00000000..dfd7381f --- /dev/null +++ b/binding/post_form.go @@ -0,0 +1,26 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import "net/http" + +type postFormBinding struct{} + +func (_ postFormBinding) Name() string { + return "post_form" +} + +func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := mapForm(obj, req.PostForm); err != nil { + return err + } + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/binding/xml.go b/binding/xml.go new file mode 100644 index 00000000..70f62932 --- /dev/null +++ b/binding/xml.go @@ -0,0 +1,27 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "encoding/xml" + "net/http" +) + +type xmlBinding struct{} + +func (_ xmlBinding) Name() string { + return "xml" +} + +func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { + decoder := xml.NewDecoder(req.Body) + if err := decoder.Decode(obj); err != nil { + return err + } + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/context.go b/context.go index 5d7e02a9..4fad861f 100644 --- a/context.go +++ b/context.go @@ -5,92 +5,46 @@ package gin import ( - "bytes" "errors" "fmt" + "math" + "net/http" + "strings" + "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" - "log" - "net" - "net/http" - "strings" ) -const ( - ErrorTypeInternal = 1 << iota - ErrorTypeExternal = 1 << iota - ErrorTypeAll = 0xffffffff -) - -// Used internally to collect errors that occurred during an http request. -type errorMsg struct { - Err string `json:"error"` - Type uint32 `json:"-"` - Meta interface{} `json:"meta"` -} - -type errorMsgs []errorMsg - -func (a errorMsgs) ByType(typ uint32) errorMsgs { - if len(a) == 0 { - return a - } - result := make(errorMsgs, 0, len(a)) - for _, msg := range a { - if msg.Type&typ > 0 { - result = append(result, msg) - } - } - return result -} - -func (a errorMsgs) String() string { - if len(a) == 0 { - return "" - } - var buffer bytes.Buffer - for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) - buffer.WriteString(text) - } - return buffer.String() -} +const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { + Engine *Engine writermem responseWriter Request *http.Request Writer ResponseWriter - Keys map[string]interface{} - Errors errorMsgs - Params httprouter.Params - Engine *Engine - handlers []HandlerFunc - index int8 - accepted []string + + Params httprouter.Params + Input inputHolder + handlers []HandlerFunc + index int8 + + Keys map[string]interface{} + Errors errorMsgs + Accepted []string } /************************************/ /********** CONTEXT CREATION ********/ /************************************/ -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - c := engine.pool.Get().(*Context) - c.writermem.reset(w) - c.Request = req - c.Params = params - c.handlers = handlers +func (c *Context) reset() { c.Keys = nil c.index = -1 - c.accepted = nil + c.Accepted = nil c.Errors = c.Errors[0:0] - return c -} - -func (engine *Engine) reuseContext(c *Context) { - engine.pool.Put(c) } func (c *Context) Copy() *Context { @@ -115,7 +69,7 @@ func (c *Context) Next() { } } -// Forces the system to do not continue calling the pending handlers in the chain. +// Forces the system to not continue calling the pending handlers in the chain. func (c *Context) Abort() { c.index = AbortIndex } @@ -127,6 +81,10 @@ func (c *Context) AbortWithStatus(code int) { c.Abort() } +func (c *Context) IsAborted() bool { + return c.index == AbortIndex +} + /************************************/ /********* ERROR MANAGEMENT *********/ /************************************/ @@ -142,7 +100,7 @@ func (c *Context) Fail(code int, err error) { c.AbortWithStatus(code) } -func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { +func (c *Context) ErrorTyped(err error, typ int, meta interface{}) { c.Errors = append(c.Errors, errorMsg{ Err: err.Error(), Type: typ, @@ -180,109 +138,43 @@ func (c *Context) Set(key string, item interface{}) { } // Get returns the value for the given key or an error if the key does not exist. -func (c *Context) Get(key string) (interface{}, error) { +func (c *Context) Get(key string) (value interface{}, ok bool) { if c.Keys != nil { - value, ok := c.Keys[key] - if ok { - return value, nil - } + value, ok = c.Keys[key] } - return nil, errors.New("Key does not exist.") + return } // MustGet returns the value for the given key or panics if the value doesn't exist. func (c *Context) MustGet(key string) interface{} { - value, err := c.Get(key) - if err != nil || value == nil { - log.Panicf("Key %s doesn't exist", value) + if value, exists := c.Get(key); exists { + return value + } else { + panic("Key " + key + " does not exist") } - return value -} - -func ipInMasks(ip net.IP, masks []interface{}) bool { - for _, proxy := range masks { - var mask *net.IPNet - var err error - - switch t := proxy.(type) { - case string: - if _, mask, err = net.ParseCIDR(t); err != nil { - panic(err) - } - case net.IP: - mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} - case net.IPNet: - mask = &t - } - - if mask.Contains(ip) { - return true - } - } - - return false -} - -// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this -// middleware if you've got servers in front of this server. The list with (known) proxies and -// local ips are being filtered out of the forwarded for list, giving the last not local ip being -// the real client ip. -func ForwardedFor(proxies ...interface{}) HandlerFunc { - if len(proxies) == 0 { - // default to local ips - var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} - - proxies = make([]interface{}, len(reservedLocalIps)) - - for i, v := range reservedLocalIps { - proxies[i] = v - } - } - - return func(c *Context) { - // the X-Forwarded-For header contains an array with left most the client ip, then - // comma separated, all proxies the request passed. The last proxy appears - // as the remote address of the request. Returning the client - // ip to comply with default RemoteAddr response. - - // check if remoteaddr is local ip or in list of defined proxies - remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) - - if !ipInMasks(remoteIp, proxies) { - return - } - - if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { - parts := strings.Split(forwardedFor, ",") - - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - - ip := net.ParseIP(strings.TrimSpace(part)) - - if ipInMasks(ip, proxies) { - continue - } - - // returning remote addr conform the original remote addr format - c.Request.RemoteAddr = ip.String() + ":0" - - // remove forwarded for address - c.Request.Header.Set("X-Forwarded-For", "") - return - } - } - } -} - -func (c *Context) ClientIP() string { - return c.Request.RemoteAddr } /************************************/ /********* PARSING REQUEST **********/ /************************************/ +func (c *Context) ClientIP() string { + clientIP := c.Request.Header.Get("X-Real-IP") + if len(clientIP) > 0 { + return clientIP + } + clientIP = c.Request.Header.Get("X-Forwarded-For") + clientIP = strings.Split(clientIP, ",")[0] + if len(clientIP) > 0 { + return strings.TrimSpace(clientIP) + } + return c.Request.RemoteAddr +} + +func (c *Context) ContentType() string { + return filterFlags(c.Request.Header.Get("Content-Type")) +} + // This function checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding @@ -290,21 +182,7 @@ func (c *Context) ClientIP() string { // else --> returns an error // if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid. func (c *Context) Bind(obj interface{}) bool { - var b binding.Binding - ctype := filterFlags(c.Request.Header.Get("Content-Type")) - switch { - case c.Request.Method == "GET" || ctype == MIMEPOSTForm: - b = binding.Form - case ctype == MIMEMultipartPOSTForm: - b = binding.MultipartForm - case ctype == MIMEJSON: - b = binding.JSON - case ctype == MIMEXML || ctype == MIMEXML2: - b = binding.XML - default: - c.Fail(400, errors.New("unknown content-type: "+ctype)) - return false - } + b := binding.Default(c.Request.Method, c.ContentType()) return c.BindWith(obj, b) } @@ -359,9 +237,9 @@ func (c *Context) HTMLString(code int, format string, values ...interface{}) { // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { if code >= 300 && code <= 308 { - c.Render(code, render.Redirect, location) + c.Render(code, render.Redirect, c.Request, location) } else { - panic(fmt.Sprintf("Cannot send a redirect with status code %d", code)) + panic(fmt.Sprintf("Cannot redirect with status code %d", code)) } } @@ -394,18 +272,18 @@ type Negotiate struct { func (c *Context) Negotiate(code int, config Negotiate) { switch c.NegotiateFormat(config.Offered...) { - case MIMEJSON: + case binding.MIMEJSON: data := chooseData(config.JSONData, config.Data) c.JSON(code, data) - case MIMEHTML: - data := chooseData(config.HTMLData, config.Data) + case binding.MIMEHTML: if len(config.HTMLPath) == 0 { panic("negotiate config is wrong. html path is needed") } + data := chooseData(config.HTMLData, config.Data) c.HTML(code, config.HTMLPath, data) - case MIMEXML: + case binding.MIMEXML: data := chooseData(config.XMLData, config.Data) c.XML(code, data) @@ -418,24 +296,22 @@ func (c *Context) NegotiateFormat(offered ...string) string { if len(offered) == 0 { panic("you must provide at least one offer") } - if c.accepted == nil { - c.accepted = parseAccept(c.Request.Header.Get("Accept")) + if c.Accepted == nil { + c.Accepted = parseAccept(c.Request.Header.Get("Accept")) } - if len(c.accepted) == 0 { + if len(c.Accepted) == 0 { return offered[0] - - } else { - for _, accepted := range c.accepted { - for _, offert := range offered { - if accepted == offert { - return offert - } + } + for _, accepted := range c.Accepted { + for _, offert := range offered { + if accepted == offert { + return offert } } - return "" } + return "" } func (c *Context) SetAccepted(formats ...string) { - c.accepted = formats + c.Accepted = formats } diff --git a/context_test.go b/context_test.go index 745e1cdc..36e4a595 100644 --- a/context_test.go +++ b/context_test.go @@ -11,509 +11,311 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/gin-gonic/gin/binding" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" ) -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil) - w := httptest.NewRecorder() - name := "" +func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { + w = httptest.NewRecorder() + r = New() + c = r.allocateContext() + c.reset() + c.writermem.reset(w) + return +} - r := New() - r.GET("/test/:name", func(c *Context) { - name = c.Params.ByName("name") - }) +func TestContextReset(t *testing.T) { + router := New() + c := router.allocateContext() + assert.Equal(t, c.Engine, router) - r.ServeHTTP(w, req) + c.index = 2 + c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} + c.Params = httprouter.Params{httprouter.Param{}} + c.Error(errors.New("test"), nil) + c.Set("foo", "bar") + c.reset() - if name != "alexandernyquist" { - t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name) - } + assert.False(t, c.IsAborted()) + assert.Nil(t, c.Keys) + assert.Nil(t, c.Accepted) + assert.Len(t, c.Errors, 0) + assert.Len(t, c.Params, 0) + assert.Equal(t, c.index, -1) + assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) } // TestContextSetGet tests that a parameter is set correctly on the // current context and can be retrieved using Get. func TestContextSetGet(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() + c, _, _ := createTestContext() + c.Set("foo", "bar") - r := New() - r.GET("/test", func(c *Context) { - // Key should be lazily created - if c.Keys != nil { - t.Error("Keys should be nil") - } + value, err := c.Get("foo") + assert.Equal(t, value, "bar") + assert.True(t, err) - // Set - c.Set("foo", "bar") + value, err = c.Get("foo2") + assert.Nil(t, value) + assert.False(t, err) - v, err := c.Get("foo") - if err != nil { - t.Errorf("Error on exist key") - } - if v != "bar" { - t.Errorf("Value should be bar, was %s", v) - } - }) - - r.ServeHTTP(w, req) + assert.Equal(t, c.MustGet("foo"), "bar") + assert.Panics(t, func() { c.MustGet("no_exist") }) } -// TestContextJSON tests that the response is serialized as JSON +// Tests that the response is serialized as JSON // and Content-Type is set to application/json -func TestContextJSON(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderJSON(t *testing.T) { + c, w, _ := createTestContext() + c.JSON(201, H{"foo": "bar"}) - r := New() - r.GET("/test", func(c *Context) { - c.JSON(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "{\"foo\":\"bar\"}\n" { - t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } -// TestContextHTML tests that the response executes the templates +// Tests that the response executes the templates // and responds with Content-Type set to text/html -func TestContextHTML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderHTML(t *testing.T) { + c, w, router := createTestContext() + templ, _ := template.New("t").Parse(`Hello {{.name}}`) + router.SetHTMLTemplate(templ) - r := New() - templ, _ := template.New("t").Parse(`Hello {{.Name}}`) - r.SetHTMLTemplate(templ) + c.HTML(201, "t", H{"name": "alexandernyquist"}) - type TestData struct{ Name string } - - r.GET("/test", func(c *Context) { - c.HTML(200, "t", TestData{"alexandernyquist"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "Hello alexandernyquist" { - t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextString tests that the response is returned -// with Content-Type set to text/plain -func TestContextString(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.String(200, "test") - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "test" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestContextXML tests that the response is serialized as XML // and Content-Type is set to application/xml -func TestContextXML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderXML(t *testing.T) { + c, w, _ := createTestContext() + c.XML(201, H{"foo": "bar"}) - r := New() - r.GET("/test", func(c *Context) { - c.XML(200, H{"foo": "bar"}) - }) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "bar") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") +} - r.ServeHTTP(w, req) +// TestContextString tests that the response is returned +// with Content-Type set to text/plain +func TestContextRenderString(t *testing.T) { + c, w, _ := createTestContext() + c.String(201, "test %s %d", "string", 2) - if w.Body.String() != "bar" { - t.Errorf("Response should be bar, was: %s", w.Body.String()) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "test string 2") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") +} - if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" { - t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type")) - } +// TestContextString tests that the response is returned +// with Content-Type set to text/html +func TestContextRenderHTMLString(t *testing.T) { + c, w, _ := createTestContext() + c.HTMLString(201, "%s %d", "string", 3) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "string 3") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestContextData tests that the response can be written from `bytesting` // with specified MIME type -func TestContextData(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/csv", nil) - w := httptest.NewRecorder() +func TestContextRenderData(t *testing.T) { + c, w, _ := createTestContext() + c.Data(201, "text/csv", []byte(`foo,bar`)) - r := New() - r.GET("/test/csv", func(c *Context) { - c.Data(200, "text/csv", []byte(`foo,bar`)) - }) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "foo,bar") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") +} - r.ServeHTTP(w, req) +// TODO +func TestContextRenderRedirectWithRelativePath(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) + assert.Panics(t, func() { c.Redirect(299, "/new_path") }) + assert.Panics(t, func() { c.Redirect(309, "/new_path") }) - if w.Body.String() != "foo,bar" { - t.Errorf("Response should be foo&bar, was: %s", w.Body.String()) + c.Redirect(302, "/path") + c.Writer.WriteHeaderNow() + assert.Equal(t, w.Code, 302) + assert.Equal(t, w.Header().Get("Location"), "/path") +} + +func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) + c.Redirect(302, "http://google.com") + c.Writer.WriteHeaderNow() + + assert.Equal(t, w.Code, 302) + assert.Equal(t, w.Header().Get("Location"), "http://google.com") +} + +func TestContextNegotiationFormat(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) +} + +func TestContextNegotiationFormatWithAccept(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML) + assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") +} + +func TestContextNegotiationFormatCustum(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + + c.Accepted = nil + c.SetAccepted(MIMEJSON, MIMEXML) + + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) +} + +// TestContextData tests that the response can be written from `bytesting` +// with specified MIME type +func TestContextAbortWithStatus(t *testing.T) { + c, w, _ := createTestContext() + c.index = 4 + c.AbortWithStatus(401) + c.Writer.WriteHeaderNow() + + assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.Writer.Status(), 401) + assert.Equal(t, w.Code, 401) + assert.True(t, c.IsAborted()) +} + +func TestContextError(t *testing.T) { + c, _, _ := createTestContext() + c.Error(errors.New("first error"), "some data") + assert.Equal(t, c.LastError().Error(), "first error") + assert.Len(t, c.Errors, 1) + + c.Error(errors.New("second error"), "some data 2") + assert.Equal(t, c.LastError().Error(), "second error") + assert.Len(t, c.Errors, 2) + + assert.Equal(t, c.Errors[0].Err, "first error") + assert.Equal(t, c.Errors[0].Meta, "some data") + assert.Equal(t, c.Errors[0].Type, ErrorTypeExternal) + + assert.Equal(t, c.Errors[1].Err, "second error") + assert.Equal(t, c.Errors[1].Meta, "some data 2") + assert.Equal(t, c.Errors[1].Type, ErrorTypeExternal) +} + +func TestContextTypedError(t *testing.T) { + c, _, _ := createTestContext() + c.ErrorTyped(errors.New("externo 0"), ErrorTypeExternal, nil) + c.ErrorTyped(errors.New("externo 1"), ErrorTypeExternal, nil) + c.ErrorTyped(errors.New("interno 0"), ErrorTypeInternal, nil) + c.ErrorTyped(errors.New("externo 2"), ErrorTypeExternal, nil) + c.ErrorTyped(errors.New("interno 1"), ErrorTypeInternal, nil) + c.ErrorTyped(errors.New("interno 2"), ErrorTypeInternal, nil) + + for _, err := range c.Errors.ByType(ErrorTypeExternal) { + assert.Equal(t, err.Type, ErrorTypeExternal) } - if w.HeaderMap.Get("Content-Type") != "text/csv" { - t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type")) + for _, err := range c.Errors.ByType(ErrorTypeInternal) { + assert.Equal(t, err.Type, ErrorTypeInternal) } } -func TestContextFile(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/file", nil) - w := httptest.NewRecorder() +func TestContextFail(t *testing.T) { + c, w, _ := createTestContext() + c.Fail(401, errors.New("bad input")) + c.Writer.WriteHeaderNow() - r := New() - r.GET("/test/file", func(c *Context) { - c.File("./gin.go") - }) - - r.ServeHTTP(w, req) - - bodyAsString := w.Body.String() - - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file data") - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 401) + assert.Equal(t, c.LastError().Error(), "bad input") + assert.Equal(t, c.index, AbortIndex) + assert.True(t, c.IsAborted()) } -// TestHandlerFunc - ensure that custom middleware works properly -func TestHandlerFunc(t *testing.T) { +func TestContextClientIP(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) - req, _ := http.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() + c.Request.Header.Set("X-Real-IP", "10.10.10.10") + c.Request.Header.Set("X-Forwarded-For", "20.20.20.20 , 30.30.30.30") + c.Request.RemoteAddr = "40.40.40.40" - r := New() - var stepsPassed int = 0 - - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - r.ServeHTTP(w, req) - - if w.Code != 404 { - t.Errorf("Response code should be Not found, was: %s", w.Code) - } - - if stepsPassed != 2 { - t.Errorf("Falied to switch context in handler function: %s", stepsPassed) - } + assert.Equal(t, c.ClientIP(), "10.10.10.10") + c.Request.Header.Del("X-Real-IP") + assert.Equal(t, c.ClientIP(), "20.20.20.20") + c.Request.Header.Del("X-Forwarded-For") + assert.Equal(t, c.ClientIP(), "40.40.40.40") } -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestBadAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - // after check and abort - c.AbortWithStatus(409) - }) - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - c.AbortWithStatus(403) - }) +func TestContextContentType(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Forbiden, was: %d", w.Code) - } - if stepsPassed != 4 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } + assert.Equal(t, c.ContentType(), "application/json") } -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.AbortWithStatus(409) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Conflict, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) +func TestContextAutoBind(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` } + assert.True(t, c.Bind(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, w.Body.Len(), 0) } -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.Fail(500, errors.New("foo")) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Server error, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) +func TestContextBadAutoBind(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` } + + assert.False(t, c.IsAborted()) + assert.False(t, c.Bind(&obj)) + c.Writer.WriteHeaderNow() + + assert.Empty(t, obj.Bar) + assert.Empty(t, obj.Foo) + assert.Equal(t, w.Code, 400) + assert.True(t, c.IsAborted()) } -func TestBindingJSON(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONEncoding(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"嘉\"}\n" { - t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONNoContentType(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) - } - - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONMalformed(t *testing.T) { - - body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) - } - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingForm(t *testing.T) { - - body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890")) - - r := New() - r.POST("/binding/form", func(c *Context) { - var body struct { - Foo string `form:"foo"` - Num int `form:"num"` - Unum uint `form:"unum"` - } - if c.Bind(&body) { - c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/form", body) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n" - if w.Body.String() != expected { - t.Errorf("Response should be %s, was %s", expected, w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestClientIP(t *testing.T) { - r := New() - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "clientip:1234" - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "clientip:1234" { - t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) - } -} - -func TestClientIPWithXForwardedForWithProxy(t *testing.T) { - r := New() - r.Use(ForwardedFor()) - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "172.16.8.3:1234" - req.Header.Set("X-Real-Ip", "realip") - req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4") - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "1.2.3.4:0" { - t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP) +func TestContextBindWith(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEXML) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` } + assert.True(t, c.BindWith(&obj, binding.JSON)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, w.Body.Len(), 0) } diff --git a/debug.go b/debug.go new file mode 100644 index 00000000..6c04aa04 --- /dev/null +++ b/debug.go @@ -0,0 +1,30 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "log" + "os" +) + +var debugLogger = log.New(os.Stdout, "[GIN-debug] ", 0) + +func IsDebugging() bool { + return ginMode == debugCode +} + +func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { + if IsDebugging() { + nuHandlers := len(handlers) + handlerName := nameOfFunction(handlers[nuHandlers-1]) + debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + } +} + +func debugPrint(format string, values ...interface{}) { + if IsDebugging() { + debugLogger.Printf(format, values...) + } +} diff --git a/debug_test.go b/debug_test.go new file mode 100644 index 00000000..05e648f9 --- /dev/null +++ b/debug_test.go @@ -0,0 +1,38 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsDebugging(t *testing.T) { + SetMode(DebugMode) + assert.True(t, IsDebugging()) + SetMode(ReleaseMode) + assert.False(t, IsDebugging()) + SetMode(TestMode) + assert.False(t, IsDebugging()) +} + +// TODO +// func TestDebugPrint(t *testing.T) { +// buffer := bytes.NewBufferString("") +// debugLogger. +// log.SetOutput(buffer) + +// SetMode(ReleaseMode) +// debugPrint("This is a example") +// assert.Equal(t, buffer.Len(), 0) + +// SetMode(DebugMode) +// debugPrint("This is %s", "a example") +// assert.Equal(t, buffer.String(), "[GIN-debug] This is a example") + +// SetMode(TestMode) +// log.SetOutput(os.Stdout) +// } diff --git a/deprecated.go b/deprecated.go index 71881530..ebee67f5 100644 --- a/deprecated.go +++ b/deprecated.go @@ -5,8 +5,22 @@ package gin import ( - "github.com/gin-gonic/gin/binding" + "log" + "net" "net/http" + "strings" + + "github.com/gin-gonic/gin/binding" +) + +const ( + MIMEJSON = binding.MIMEJSON + MIMEHTML = binding.MIMEHTML + MIMEXML = binding.MIMEXML + MIMEXML2 = binding.MIMEXML2 + MIMEPlain = binding.MIMEPlain + MIMEPOSTForm = binding.MIMEPOSTForm + MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) // DEPRECATED, use Bind() instead. @@ -45,3 +59,79 @@ func (engine *Engine) LoadHTMLTemplates(pattern string) { func (engine *Engine) NotFound404(handlers ...HandlerFunc) { engine.NoRoute(handlers...) } + +// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this +// middleware if you've got servers in front of this server. The list with (known) proxies and +// local ips are being filtered out of the forwarded for list, giving the last not local ip being +// the real client ip. +func ForwardedFor(proxies ...interface{}) HandlerFunc { + if len(proxies) == 0 { + // default to local ips + var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} + + proxies = make([]interface{}, len(reservedLocalIps)) + + for i, v := range reservedLocalIps { + proxies[i] = v + } + } + + return func(c *Context) { + // the X-Forwarded-For header contains an array with left most the client ip, then + // comma separated, all proxies the request passed. The last proxy appears + // as the remote address of the request. Returning the client + // ip to comply with default RemoteAddr response. + + // check if remoteaddr is local ip or in list of defined proxies + remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) + + if !ipInMasks(remoteIp, proxies) { + return + } + + if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { + parts := strings.Split(forwardedFor, ",") + + for i := len(parts) - 1; i >= 0; i-- { + part := parts[i] + + ip := net.ParseIP(strings.TrimSpace(part)) + + if ipInMasks(ip, proxies) { + continue + } + + // returning remote addr conform the original remote addr format + c.Request.RemoteAddr = ip.String() + ":0" + + // remove forwarded for address + c.Request.Header.Set("X-Forwarded-For", "") + return + } + } + } +} + +func ipInMasks(ip net.IP, masks []interface{}) bool { + for _, proxy := range masks { + var mask *net.IPNet + var err error + + switch t := proxy.(type) { + case string: + if _, mask, err = net.ParseCIDR(t); err != nil { + log.Panic(err) + } + case net.IP: + mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} + case net.IPNet: + mask = &t + } + + if mask.Contains(ip) { + return true + } + } + + return false +} diff --git a/errors.go b/errors.go new file mode 100644 index 00000000..819c2941 --- /dev/null +++ b/errors.go @@ -0,0 +1,50 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "fmt" +) + +const ( + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAll = 0xffffffff +) + +// Used internally to collect errors that occurred during an http request. +type errorMsg struct { + Err string `json:"error"` + Type int `json:"-"` + Meta interface{} `json:"meta"` +} + +type errorMsgs []errorMsg + +func (a errorMsgs) ByType(typ int) errorMsgs { + if len(a) == 0 { + return a + } + result := make(errorMsgs, 0, len(a)) + for _, msg := range a { + if msg.Type&typ > 0 { + result = append(result, msg) + } + } + return result +} + +func (a errorMsgs) String() string { + if len(a) == 0 { + return "" + } + var buffer bytes.Buffer + for i, msg := range a { + text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) + buffer.WriteString(text) + } + return buffer.String() +} diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go index 9f745e1e..9b79deb5 100644 --- a/examples/pluggable_renderer/example_pongo2.go +++ b/examples/pluggable_renderer/example_pongo2.go @@ -1,11 +1,26 @@ package main import ( + "net/http" + "github.com/flosch/pongo2" "github.com/gin-gonic/gin" - "net/http" + "github.com/gin-gonic/gin/render" ) +func main() { + router := gin.Default() + router.HTMLRender = newPongoRender() + + router.GET("/index", func(c *gin.Context) { + c.HTML(200, "index.html", gin.H{ + "title": "Gin meets pongo2 !", + "name": c.Input.Get("name"), + }) + }) + router.Run(":8080") +} + type pongoRender struct { cache map[string]*pongo2.Template } @@ -14,13 +29,6 @@ func newPongoRender() *pongoRender { return &pongoRender{map[string]*pongo2.Template{}} } -func writeHeader(w http.ResponseWriter, code int, contentType string) { - if code >= 0 { - w.Header().Set("Content-Type", contentType) - w.WriteHeader(code) - } -} - func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { file := data[0].(string) ctx := data[1].(pongo2.Context) @@ -36,23 +44,6 @@ func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{ p.cache[file] = tmpl t = tmpl } - writeHeader(w, code, "text/html") + render.WriteHeader(w, code, "text/html") return t.ExecuteWriter(ctx, w) } - -func main() { - r := gin.Default() - r.HTMLRender = newPongoRender() - - r.GET("/index", func(c *gin.Context) { - name := c.Request.FormValue("name") - ctx := pongo2.Context{ - "title": "Gin meets pongo2 !", - "name": name, - } - c.HTML(200, "index.html", ctx) - }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") -} diff --git a/gin.go b/gin.go index c23577df..7cf4de5e 100644 --- a/gin.go +++ b/gin.go @@ -5,24 +5,15 @@ package gin import ( - "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" "html/template" - "math" "net/http" "sync" + + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + "github.com/julienschmidt/httprouter" ) -const ( - AbortIndex = math.MaxInt8 / 2 - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" - MIMEMultipartPOSTForm = "multipart/form-data" -) type ( HandlerFunc func(*Context) @@ -30,14 +21,15 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - Default404Body []byte - Default405Body []byte - pool sync.Pool - allNoRouteNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + Default404Body []byte + Default405Body []byte + pool sync.Pool + allNoRoute []HandlerFunc + allNoMethod []HandlerFunc + noRoute []HandlerFunc + noMethod []HandlerFunc + router *httprouter.Router } ) @@ -56,9 +48,7 @@ func New() *Engine { engine.router.NotFound = engine.handle404 engine.router.MethodNotAllowed = engine.handle405 engine.pool.New = func() interface{} { - c := &Context{Engine: engine} - c.Writer = &c.writermem - return c + return engine.allocateContext() } return engine } @@ -70,10 +60,30 @@ func Default() *Engine { return engine } +func (engine *Engine) allocateContext() (context *Context) { + context = &Context{Engine: engine} + context.Writer = &context.writermem + context.Input = inputHolder{context: context} + return +} + +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + c := engine.pool.Get().(*Context) + c.reset() + c.writermem.reset(w) + c.Request = req + c.Params = params + c.handlers = handlers + return c +} + +func (engine *Engine) reuseContext(c *Context) { + engine.pool.Put(c) +} + func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { - render.HTMLDebug.AddGlob(pattern) - engine.HTMLRender = render.HTMLDebug + engine.HTMLRender = &render.HTMLDebugRender{Glob: pattern} } else { templ := template.Must(template.ParseGlob(pattern)) engine.SetHTMLTemplate(templ) @@ -82,8 +92,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { - render.HTMLDebug.AddFiles(files...) - engine.HTMLRender = render.HTMLDebug + engine.HTMLRender = &render.HTMLDebugRender{Files: files} } else { templ := template.Must(template.ParseFiles(files...)) engine.SetHTMLTemplate(templ) @@ -114,21 +123,21 @@ func (engine *Engine) Use(middlewares ...HandlerFunc) { } func (engine *Engine) rebuild404Handlers() { - engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute) + engine.allNoRoute = engine.combineHandlers(engine.noRoute) } func (engine *Engine) rebuild405Handlers() { - engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod) + engine.allNoMethod = engine.combineHandlers(engine.noMethod) } func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod) + c := engine.createContext(w, req, nil, engine.allNoRoute) // set 404 by default, useful for logging c.Writer.WriteHeader(404) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 404 { - c.Data(-1, MIMEPlain, engine.Default404Body) + c.Data(-1, binding.MIMEPlain, engine.Default404Body) } else { c.Writer.WriteHeaderNow() } @@ -137,13 +146,13 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { } func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod) + c := engine.createContext(w, req, nil, engine.allNoMethod) // set 405 by default, useful for logging c.Writer.WriteHeader(405) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 405 { - c.Data(-1, MIMEPlain, engine.Default405Body) + c.Data(-1, binding.MIMEPlain, engine.Default405Body) } else { c.Writer.WriteHeaderNow() } @@ -158,16 +167,10 @@ func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Reques func (engine *Engine) Run(addr string) error { debugPrint("Listening and serving HTTP on %s\n", addr) - if err := http.ListenAndServe(addr, engine); err != nil { - return err - } - return nil + return http.ListenAndServe(addr, engine) } func (engine *Engine) RunTLS(addr string, cert string, key string) error { debugPrint("Listening and serving HTTPS on %s\n", addr) - if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { - return err - } - return nil + return http.ListenAndServeTLS(addr, cert, key, engine) } diff --git a/gin_test.go b/gin_test.go index ba74c159..baac9764 100644 --- a/gin_test.go +++ b/gin_test.go @@ -5,202 +5,137 @@ package gin import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" "testing" + + "github.com/stretchr/testify/assert" ) func init() { SetMode(TestMode) } -func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { - req, _ := http.NewRequest(method, path, nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - return w +func TestCreateEngine(t *testing.T) { + router := New() + assert.Equal(t, "/", router.absolutePath) + assert.Equal(t, router.engine, router) + assert.Empty(t, router.Handlers) + + // TODO + // assert.Equal(t, router.router.NotFound, router.handle404) + // assert.Equal(t, router.router.MethodNotAllowed, router.handle405) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == false { - t.Errorf(method + " route handler was not invoked.") - } - if w.Code != http.StatusOK { - t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code) - } -} -func TestRouterGroupRouteOK(t *testing.T) { - testRouteOK("POST", t) - testRouteOK("DELETE", t) - testRouteOK("PATCH", t) - testRouteOK("PUT", t) - testRouteOK("OPTIONS", t) - testRouteOK("HEAD", t) +func TestCreateDefaultRouter(t *testing.T) { + router := Default() + assert.Len(t, router.Handlers, 2) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestNoRouteWithoutGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} - // RUN - w := PerformRequest(r, method, "/test") + router := New() - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) - } + router.NoRoute(middleware0) + assert.Nil(t, router.Handlers) + assert.Len(t, router.noRoute, 1) + assert.Len(t, router.allNoRoute, 1) + assert.Equal(t, router.noRoute[0], middleware0) + assert.Equal(t, router.allNoRoute[0], middleware0) + + router.NoRoute(middleware1, middleware0) + assert.Len(t, router.noRoute, 2) + assert.Len(t, router.allNoRoute, 2) + assert.Equal(t, router.noRoute[0], middleware1) + assert.Equal(t, router.allNoRoute[0], middleware1) + assert.Equal(t, router.noRoute[1], middleware0) + assert.Equal(t, router.allNoRoute[1], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK(t *testing.T) { - testRouteNotOK("POST", t) - testRouteNotOK("DELETE", t) - testRouteNotOK("PATCH", t) - testRouteNotOK("PUT", t) - testRouteNotOK("OPTIONS", t) - testRouteNotOK("HEAD", t) +func TestNoRouteWithGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} + middleware2 := func(c *Context) {} + + router := New() + router.Use(middleware2) + + router.NoRoute(middleware0) + assert.Len(t, router.allNoRoute, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noRoute, 1) + + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.noRoute[0], middleware0) + assert.Equal(t, router.allNoRoute[0], middleware2) + assert.Equal(t, router.allNoRoute[1], middleware0) + + router.Use(middleware1) + assert.Len(t, router.allNoRoute, 3) + assert.Len(t, router.Handlers, 2) + assert.Len(t, router.noRoute, 1) + + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.Handlers[1], middleware1) + assert.Equal(t, router.noRoute[0], middleware0) + assert.Equal(t, router.allNoRoute[0], middleware2) + assert.Equal(t, router.allNoRoute[1], middleware1) + assert.Equal(t, router.allNoRoute[2], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK2(method string, t *testing.T) { - // SETUP - passed := false - r := New() - var methodRoute string - if method == "POST" { - methodRoute = "GET" - } else { - methodRoute = "POST" - } - r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestNoMethodWithoutGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} - // RUN - w := PerformRequest(r, method, "/test") + router := New() - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location")) - } + router.NoMethod(middleware0) + assert.Empty(t, router.Handlers) + assert.Len(t, router.noMethod, 1) + assert.Len(t, router.allNoMethod, 1) + assert.Equal(t, router.noMethod[0], middleware0) + assert.Equal(t, router.allNoMethod[0], middleware0) + + router.NoMethod(middleware1, middleware0) + assert.Len(t, router.noMethod, 2) + assert.Len(t, router.allNoMethod, 2) + assert.Equal(t, router.noMethod[0], middleware1) + assert.Equal(t, router.allNoMethod[0], middleware1) + assert.Equal(t, router.noMethod[1], middleware0) + assert.Equal(t, router.allNoMethod[1], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK2(t *testing.T) { - testRouteNotOK2("POST", t) - testRouteNotOK2("DELETE", t) - testRouteNotOK2("PATCH", t) - testRouteNotOK2("PUT", t) - testRouteNotOK2("OPTIONS", t) - testRouteNotOK2("HEAD", t) +func TestRebuild404Handlers(t *testing.T) { + } -// TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { - // SETUP file - testRoot, _ := os.Getwd() - f, err := ioutil.TempFile(testRoot, "") - if err != nil { - t.Error(err) - } - defer os.Remove(f.Name()) - filePath := path.Join("/", path.Base(f.Name())) - f.WriteString("Gin Web Framework") - f.Close() +func TestNoMethodWithGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} + middleware2 := func(c *Context) {} - // SETUP gin - r := New() - r.Static("./", testRoot) + router := New() + router.Use(middleware2) - // RUN - w := PerformRequest(r, "GET", filePath) + router.NoMethod(middleware0) + assert.Len(t, router.allNoMethod, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noMethod, 1) - // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "HEAD", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.noMethod[0], middleware0) + assert.Equal(t, router.allNoMethod[0], middleware2) + assert.Equal(t, router.allNoMethod[1], middleware0) + + router.Use(middleware1) + assert.Len(t, router.allNoMethod, 3) + assert.Len(t, router.Handlers, 2) + assert.Len(t, router.noMethod, 1) + + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.Handlers[1], middleware1) + assert.Equal(t, router.noMethod[0], middleware0) + assert.Equal(t, router.allNoMethod[0], middleware2) + assert.Equal(t, router.allNoMethod[1], middleware1) + assert.Equal(t, router.allNoMethod[2], middleware0) } diff --git a/input_holder.go b/input_holder.go new file mode 100644 index 00000000..aa5fca99 --- /dev/null +++ b/input_holder.go @@ -0,0 +1,47 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +type inputHolder struct { + context *Context +} + +func (i inputHolder) FromGET(key string) (va string) { + va, _ = i.fromGET(key) + return +} + +func (i inputHolder) FromPOST(key string) (va string) { + va, _ = i.fromPOST(key) + return +} + +func (i inputHolder) Get(key string) string { + if value, exists := i.fromPOST(key); exists { + return value + } + if value, exists := i.fromGET(key); exists { + return value + } + return "" +} + +func (i inputHolder) fromGET(key string) (string, bool) { + req := i.context.Request + req.ParseForm() + if values, ok := req.Form[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} + +func (i inputHolder) fromPOST(key string) (string, bool) { + req := i.context.Request + req.ParseForm() + if values, ok := req.PostForm[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} diff --git a/logger.go b/logger.go index 0f1f34b1..87304dd5 100644 --- a/logger.go +++ b/logger.go @@ -5,8 +5,8 @@ package gin import ( - "github.com/mattn/go-colorable" - "log" + "fmt" + "io" "time" ) @@ -25,25 +25,27 @@ func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAll) } -func ErrorLoggerT(typ uint32) HandlerFunc { +func ErrorLoggerT(typ int) HandlerFunc { return func(c *Context) { c.Next() - errs := c.Errors.ByType(typ) - if len(errs) > 0 { - // -1 status code = do not change current one - c.JSON(-1, c.Errors) + if !c.Writer.Written() { + if errs := c.Errors.ByType(typ); len(errs) > 0 { + c.JSON(-1, errs) + } } } } func Logger() HandlerFunc { - stdlogger := log.New(colorable.NewColorableStdout(), "", 0) - //errlogger := log.New(os.Stderr, "", 0) + return LoggerWithFile(DefaultWriter) +} +func LoggerWithFile(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() + path := c.Request.URL.Path // Process request c.Next() @@ -57,15 +59,16 @@ func Logger() HandlerFunc { statusCode := c.Writer.Status() statusColor := colorForStatus(statusCode) methodColor := colorForMethod(method) + comment := c.Errors.String() - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", + fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", end.Format("2006/01/02 - 15:04:05"), statusColor, statusCode, reset, latency, clientIP, methodColor, reset, method, - c.Request.URL.Path, - c.Errors.String(), + path, + comment, ) } } diff --git a/mode.go b/mode.go index 0495b830..8c54fdb6 100644 --- a/mode.go +++ b/mode.go @@ -5,8 +5,9 @@ package gin import ( - "fmt" "os" + + "github.com/mattn/go-colorable" ) const GIN_MODE = "GIN_MODE" @@ -22,8 +23,9 @@ const ( testCode = iota ) -var gin_mode int = debugCode -var mode_name string = DebugMode +var DefaultWriter = colorable.NewColorableStdout() +var ginMode int = debugCode +var modeName string = DebugMode func init() { value := os.Getenv(GIN_MODE) @@ -37,27 +39,17 @@ func init() { func SetMode(value string) { switch value { case DebugMode: - gin_mode = debugCode + ginMode = debugCode case ReleaseMode: - gin_mode = releaseCode + ginMode = releaseCode case TestMode: - gin_mode = testCode + ginMode = testCode default: panic("gin mode unknown: " + value) } - mode_name = value + modeName = value } func Mode() string { - return mode_name -} - -func IsDebugging() bool { - return gin_mode == debugCode -} - -func debugPrint(format string, values ...interface{}) { - if IsDebugging() { - fmt.Printf("[GIN-debug] "+format, values...) - } + return modeName } diff --git a/recovery_test.go b/recovery_test.go index f9047e24..32eb3ee5 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -22,13 +22,13 @@ func TestPanicInHandler(t *testing.T) { }) // RUN - w := PerformRequest(r, "GET", "/recovery") + w := performRequest(r, "GET", "/recovery") // restore logging log.SetOutput(os.Stderr) if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %s", w.Code) + t.Errorf("Response code should be Internal Server Error, was: %d", w.Code) } } @@ -44,13 +44,13 @@ func TestPanicWithAbort(t *testing.T) { }) // RUN - w := PerformRequest(r, "GET", "/recovery") + w := performRequest(r, "GET", "/recovery") // restore logging log.SetOutput(os.Stderr) // TEST if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) + t.Errorf("Response code should be Bad request, was: %d", w.Code) } } diff --git a/render/html_debug.go b/render/html_debug.go new file mode 100644 index 00000000..1edac5df --- /dev/null +++ b/render/html_debug.go @@ -0,0 +1,42 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "html/template" + "net/http" +) + +type HTMLDebugRender struct { + Files []string + Glob string +} + +func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "text/html") + file := data[0].(string) + obj := data[1] + + if t, err := r.newTemplate(); err == nil { + return t.ExecuteTemplate(w, file, obj) + } else { + return err + } +} + +func (r *HTMLDebugRender) newTemplate() (*template.Template, error) { + t := template.New("") + if len(r.Files) > 0 { + if _, err := t.ParseFiles(r.Files...); err != nil { + return nil, err + } + } + if len(r.Glob) > 0 { + if _, err := t.ParseGlob(r.Glob); err != nil { + return nil, err + } + } + return t, nil +} diff --git a/render/render.go b/render/render.go index bc7bceb8..525adae6 100644 --- a/render/render.go +++ b/render/render.go @@ -17,124 +17,112 @@ type ( Render(http.ResponseWriter, int, ...interface{}) error } - // JSON binding jsonRender struct{} - // XML binding + indentedJSON struct{} + xmlRender struct{} - // Plain text - plainRender struct{} + plainTextRender struct{} - // HTML Plain text htmlPlainRender struct{} - // Redirects redirectRender struct{} - // Redirects - htmlDebugRender struct { - files []string - globs []string - } - - // form binding HTMLRender struct { Template *template.Template } ) var ( - JSON = jsonRender{} - XML = xmlRender{} - Plain = plainRender{} - HTMLPlain = htmlPlainRender{} - Redirect = redirectRender{} - HTMLDebug = &htmlDebugRender{} + JSON = jsonRender{} + IndentedJSON = indentedJSON{} + XML = xmlRender{} + HTMLPlain = htmlPlainRender{} + Plain = plainTextRender{} + Redirect = redirectRender{} ) -func writeHeader(w http.ResponseWriter, code int, contentType string) { - w.Header().Set("Content-Type", contentType+"; charset=utf-8") - w.WriteHeader(code) -} - -func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "application/json") - encoder := json.NewEncoder(w) - return encoder.Encode(data[0]) -} - func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - w.Header().Set("Location", data[0].(string)) - w.WriteHeader(code) + req := data[0].(*http.Request) + location := data[1].(string) + http.Redirect(w, req, location, code) return nil } +func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "application/json") + return json.NewEncoder(w).Encode(data[0]) +} + +func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "application/json") + jsonData, err := json.MarshalIndent(data[0], "", " ") + if err != nil { + return err + } + _, err = w.Write(jsonData) + return err +} + func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "application/xml") - encoder := xml.NewEncoder(w) - return encoder.Encode(data[0]) + WriteHeader(w, code, "application/xml") + return xml.NewEncoder(w).Encode(data[0]) } -func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/plain") +func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { + WriteHeader(w, code, "text/plain") format := data[0].(string) args := data[1].([]interface{}) - var err error if len(args) > 0 { _, err = w.Write([]byte(fmt.Sprintf(format, args...))) } else { _, err = w.Write([]byte(format)) } - return err + return } -func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") +func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { + WriteHeader(w, code, "text/html") format := data[0].(string) args := data[1].([]interface{}) - var err error if len(args) > 0 { _, err = w.Write([]byte(fmt.Sprintf(format, args...))) } else { _, err = w.Write([]byte(format)) } - return err -} - -func (r *htmlDebugRender) AddGlob(pattern string) { - r.globs = append(r.globs, pattern) -} - -func (r *htmlDebugRender) AddFiles(files ...string) { - r.files = append(r.files, files...) -} - -func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") - file := data[0].(string) - obj := data[1] - - t := template.New("") - - if len(r.files) > 0 { - if _, err := t.ParseFiles(r.files...); err != nil { - return err - } - } - - for _, glob := range r.globs { - if _, err := t.ParseGlob(glob); err != nil { - return err - } - } - - return t.ExecuteTemplate(w, file, obj) + return } func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") + WriteHeader(w, code, "text/html") file := data[0].(string) - obj := data[1] - return html.Template.ExecuteTemplate(w, file, obj) + args := data[1] + return html.Template.ExecuteTemplate(w, file, args) +} + +func WriteHeader(w http.ResponseWriter, code int, contentType string) { + contentType = joinStrings(contentType, "; charset=utf-8") + w.Header().Set("Content-Type", contentType) + w.WriteHeader(code) +} + +func joinStrings(a ...string) string { + if len(a) == 0 { + return "" + } + if len(a) == 1 { + return a[0] + } + n := 0 + for i := 0; i < len(a); i++ { + n += len(a[i]) + } + + b := make([]byte, n) + n = 0 + for _, s := range a { + n += copy(b[n:], s) + } + return string(b) } diff --git a/response_writer.go b/response_writer.go index 98993958..3e8f54f2 100644 --- a/response_writer.go +++ b/response_writer.go @@ -6,14 +6,14 @@ package gin import ( "bufio" - "errors" "log" "net" "net/http" ) const ( - NoWritten = -1 + noWritten = -1 + defaultStatus = 200 ) type ( @@ -31,15 +31,15 @@ type ( responseWriter struct { http.ResponseWriter - status int size int + status int } ) func (w *responseWriter) reset(writer http.ResponseWriter) { w.ResponseWriter = writer - w.status = 200 - w.size = NoWritten + w.size = noWritten + w.status = defaultStatus } func (w *responseWriter) WriteHeader(code int) { @@ -74,16 +74,13 @@ func (w *responseWriter) Size() int { } func (w *responseWriter) Written() bool { - return w.size != NoWritten + return w.size != noWritten } // Implements the http.Hijacker interface func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hijacker, ok := w.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") - } - return hijacker.Hijack() + w.size = 0 // this prevents Gin to write the HTTP headers + return w.ResponseWriter.(http.Hijacker).Hijack() } // Implements the http.CloseNotify interface @@ -93,8 +90,5 @@ func (w *responseWriter) CloseNotify() <-chan bool { // Implements the http.Flush interface func (w *responseWriter) Flush() { - flusher, ok := w.ResponseWriter.(http.Flusher) - if ok { - flusher.Flush() - } + w.ResponseWriter.(http.Flusher).Flush() } diff --git a/routergroup.go b/routergroup.go index 8e02a402..b2a04874 100644 --- a/routergroup.go +++ b/routergroup.go @@ -5,9 +5,10 @@ package gin import ( - "github.com/julienschmidt/httprouter" "net/http" "path" + + "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix @@ -46,11 +47,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) - if IsDebugging() { - nuHandlers := len(handlers) - handlerName := nameOfFunction(handlers[nuHandlers-1]) - debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) - } + debugRoute(httpMethod, absolutePath, handlers) group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { context := group.engine.createContext(w, req, params, handlers) @@ -114,11 +111,11 @@ func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) { func (group *RouterGroup) Static(relativePath, root string) { absolutePath := group.calculateAbsolutePath(relativePath) handler := group.createStaticHandler(absolutePath, root) - absolutePath = path.Join(absolutePath, "/*filepath") + relativePath = path.Join(relativePath, "/*filepath") // Register GET and HEAD handlers - group.GET(absolutePath, handler) - group.HEAD(absolutePath, handler) + group.GET(relativePath, handler) + group.HEAD(relativePath, handler) } func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) { diff --git a/routes_test.go b/routes_test.go new file mode 100644 index 00000000..ce61a41d --- /dev/null +++ b/routes_test.go @@ -0,0 +1,332 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { + req, _ := http.NewRequest(method, path, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +func testRouteOK(method string, t *testing.T) { + // SETUP + passed := false + r := New() + r.Handle(method, "/test", []HandlerFunc{func(c *Context) { + passed = true + }}) + // RUN + w := performRequest(r, method, "/test") + + // TEST + assert.True(t, passed) + assert.Equal(t, w.Code, http.StatusOK) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func testRouteNotOK(method string, t *testing.T) { + // SETUP + passed := false + router := New() + router.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { + passed = true + }}) + + // RUN + w := performRequest(router, method, "/test") + + // TEST + assert.False(t, passed) + assert.Equal(t, w.Code, http.StatusNotFound) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func testRouteNotOK2(method string, t *testing.T) { + // SETUP + passed := false + router := New() + var methodRoute string + if method == "POST" { + methodRoute = "GET" + } else { + methodRoute = "POST" + } + router.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { + passed = true + }}) + + // RUN + w := performRequest(router, method, "/test") + + // TEST + assert.False(t, passed) + assert.Equal(t, w.Code, http.StatusMethodNotAllowed) +} + +func TestRouterGroupRouteOK(t *testing.T) { + testRouteOK("POST", t) + testRouteOK("DELETE", t) + testRouteOK("PATCH", t) + testRouteOK("PUT", t) + testRouteOK("OPTIONS", t) + testRouteOK("HEAD", t) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func TestRouteNotOK(t *testing.T) { + testRouteNotOK("POST", t) + testRouteNotOK("DELETE", t) + testRouteNotOK("PATCH", t) + testRouteNotOK("PUT", t) + testRouteNotOK("OPTIONS", t) + testRouteNotOK("HEAD", t) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func TestRouteNotOK2(t *testing.T) { + testRouteNotOK2("POST", t) + testRouteNotOK2("DELETE", t) + testRouteNotOK2("PATCH", t) + testRouteNotOK2("PUT", t) + testRouteNotOK2("OPTIONS", t) + testRouteNotOK2("HEAD", t) +} + +// TestHandleStaticFile - ensure the static file handles properly +func TestHandleStaticFile(t *testing.T) { + // SETUP file + testRoot, _ := os.Getwd() + f, err := ioutil.TempFile(testRoot, "") + if err != nil { + t.Error(err) + } + defer os.Remove(f.Name()) + filePath := path.Join("/", path.Base(f.Name())) + f.WriteString("Gin Web Framework") + f.Close() + + // SETUP gin + r := New() + r.Static("./", testRoot) + + // RUN + w := performRequest(r, "GET", filePath) + + // TEST + if w.Code != 200 { + t.Errorf("Response code should be 200, was: %d", w.Code) + } + if w.Body.String() != "Gin Web Framework" { + t.Errorf("Response should be test, was: %s", w.Body.String()) + } + if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { + t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) + } +} + +// TestHandleStaticDir - ensure the root/sub dir handles properly +func TestHandleStaticDir(t *testing.T) { + // SETUP + r := New() + r.Static("/", "./") + + // RUN + w := performRequest(r, "GET", "/") + + // TEST + bodyAsString := w.Body.String() + if w.Code != 200 { + t.Errorf("Response code should be 200, was: %d", w.Code) + } + if len(bodyAsString) == 0 { + t.Errorf("Got empty body instead of file tree") + } + if !strings.Contains(bodyAsString, "gin.go") { + t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) + } + if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { + t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) + } +} + +// TestHandleHeadToDir - ensure the root/sub dir handles properly +func TestHandleHeadToDir(t *testing.T) { + // SETUP + router := New() + router.Static("/", "./") + + // RUN + w := performRequest(router, "HEAD", "/") + + // TEST + bodyAsString := w.Body.String() + assert.Equal(t, w.Code, 200) + assert.NotEmpty(t, bodyAsString) + assert.Contains(t, bodyAsString, "gin.go") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestContextGeneralCase(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + }) + router.GET("/", func(c *Context) { + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "X" + }) + router.NoMethod(func(c *Context) { + signature += "X" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, signature, "ACDB") +} + +// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers +func TestContextNextOrder(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 404) + assert.Equal(t, signature, "ACEGHFDB") +} + +// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order +func TestAbortHandlersChain(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + }) + router.Use(func(c *Context) { + signature += "C" + c.AbortWithStatus(409) + c.Next() + signature += "D" + }) + router.GET("/", func(c *Context) { + signature += "D" + c.Next() + signature += "E" + }) + + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, signature, "ACD") + assert.Equal(t, w.Code, 409) +} + +func TestAbortHandlersChainAndNext(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.AbortWithStatus(410) + c.Next() + signature += "B" + + }) + router.GET("/", func(c *Context) { + signature += "C" + c.Next() + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, signature, "AB") + assert.Equal(t, w.Code, 410) +} + +// TestContextParamsGet tests that a parameter can be parsed from the URL. +func TestContextParamsByName(t *testing.T) { + name := "" + lastName := "" + router := New() + router.GET("/test/:name/:last_name", func(c *Context) { + name = c.Params.ByName("name") + lastName = c.Params.ByName("last_name") + }) + // RUN + w := performRequest(router, "GET", "/test/john/smith") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, name, "john") + assert.Equal(t, lastName, "smith") +} + +// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as +// as well as Abort +func TestFailHandlersChain(t *testing.T) { + // SETUP + var stepsPassed int = 0 + r := New() + r.Use(func(context *Context) { + stepsPassed += 1 + context.Fail(500, errors.New("foo")) + }) + r.Use(func(context *Context) { + stepsPassed += 1 + context.Next() + stepsPassed += 1 + }) + // RUN + w := performRequest(r, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 500, "Response code should be Server error, was: %d", w.Code) + assert.Equal(t, stepsPassed, 1, "Falied to switch context in handler function: %d", stepsPassed) +} diff --git a/utils.go b/utils.go index 43ddaecd..568311fc 100644 --- a/utils.go +++ b/utils.go @@ -56,17 +56,20 @@ func chooseData(custom, wildcard interface{}) interface{} { return custom } -func parseAccept(accept string) []string { - parts := strings.Split(accept, ",") - for i, part := range parts { +func parseAccept(acceptHeader string) []string { + parts := strings.Split(acceptHeader, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { index := strings.IndexByte(part, ';') if index >= 0 { part = part[0:index] } part = strings.TrimSpace(part) - parts[i] = part + if len(part) > 0 { + out = append(out, part) + } } - return parts + return out } func lastChar(str string) uint8 {