github.com/zak-blake/goa@v1.4.1/middleware/error_handler_test.go (about)

     1  package middleware_test
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"regexp"
     9  	"strings"
    10  
    11  	pErrors "github.com/pkg/errors"
    12  
    13  	"context"
    14  
    15  	"github.com/goadesign/goa"
    16  	"github.com/goadesign/goa/middleware"
    17  	. "github.com/onsi/ginkgo"
    18  	. "github.com/onsi/gomega"
    19  )
    20  
    21  // errorResponse contains the details of a error response. It implements ServiceError.
    22  type errorResponse struct {
    23  	// ID is the unique error instance identifier.
    24  	ID string `json:"id" yaml:"id" xml:"id" form:"id"`
    25  	// Code identifies the class of errors.
    26  	Code string `json:"code" yaml:"code" xml:"code" form:"code"`
    27  	// Status is the HTTP status code used by responses that cary the error.
    28  	Status int `json:"status" yaml:"status" xml:"status" form:"status"`
    29  	// Detail describes the specific error occurrence.
    30  	Detail string `json:"detail" yaml:"detail" xml:"detail" form:"detail"`
    31  	// Meta contains additional key/value pairs useful to clients.
    32  	Meta map[string]interface{} `json:"meta,omitempty" yaml:"meta,omitempty" xml:"meta,omitempty" form:"meta,omitempty"`
    33  }
    34  
    35  // Error returns the error occurrence details.
    36  func (e *errorResponse) Error() string {
    37  	msg := fmt.Sprintf("[%s] %d %s: %s", e.ID, e.Status, e.Code, e.Detail)
    38  	for k, v := range e.Meta {
    39  		msg += ", " + fmt.Sprintf("%s: %v", k, v)
    40  	}
    41  	return msg
    42  }
    43  
    44  var _ = Describe("ErrorHandler", func() {
    45  	var service *goa.Service
    46  	var h goa.Handler
    47  	var verbose bool
    48  
    49  	var rw *testResponseWriter
    50  
    51  	BeforeEach(func() {
    52  		service = nil
    53  		h = nil
    54  		verbose = true
    55  		rw = nil
    56  	})
    57  
    58  	JustBeforeEach(func() {
    59  		rw = newTestResponseWriter()
    60  		eh := middleware.ErrorHandler(service, verbose)(h)
    61  		req, err := http.NewRequest("GET", "/foo", nil)
    62  		Ω(err).ShouldNot(HaveOccurred())
    63  		ctx := newContext(service, rw, req, nil)
    64  		err = eh(ctx, rw, req)
    65  		Ω(err).ShouldNot(HaveOccurred())
    66  	})
    67  
    68  	Context("with a handler returning a Go error", func() {
    69  
    70  		BeforeEach(func() {
    71  			service = newService(nil)
    72  			h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
    73  				return errors.New("boom")
    74  			}
    75  		})
    76  
    77  		It("turns Go errors into HTTP 500 responses", func() {
    78  			Ω(rw.Status).Should(Equal(500))
    79  			Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{"text/plain"}))
    80  			Ω(string(rw.Body)).Should(Equal(`"boom"` + "\n"))
    81  		})
    82  
    83  		Context("not verbose", func() {
    84  			BeforeEach(func() {
    85  				verbose = false
    86  			})
    87  
    88  			It("hides the error details", func() {
    89  				var decoded errorResponse
    90  				Ω(rw.Status).Should(Equal(500))
    91  				Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
    92  				err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
    93  				Ω(err).ShouldNot(HaveOccurred())
    94  				msg := goa.ErrInternal(`Internal Server Error [zzz]`).Error()
    95  				msg = regexp.QuoteMeta(msg)
    96  				msg = strings.Replace(msg, "zzz", ".+", 1)
    97  				endIDidx := strings.Index(msg, "]")
    98  				msg = `\[.*\]` + msg[endIDidx+1:]
    99  				Ω(fmt.Sprintf("%v", decoded.Error())).Should(MatchRegexp(msg))
   100  			})
   101  
   102  			Context("and goa 500 error", func() {
   103  				var origID string
   104  
   105  				BeforeEach(func() {
   106  					h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
   107  						e := goa.ErrInternal("goa-500-boom")
   108  						origID = e.(goa.ServiceError).Token()
   109  						return e
   110  					}
   111  				})
   112  
   113  				It("preserves the error ID from the original error", func() {
   114  					var decoded errorResponse
   115  					Ω(origID).ShouldNot(Equal(""))
   116  					Ω(rw.Status).Should(Equal(500))
   117  					Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
   118  					err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
   119  					Ω(err).ShouldNot(HaveOccurred())
   120  					Ω(decoded.ID).Should(Equal(origID))
   121  				})
   122  			})
   123  
   124  			Context("and goa 504 error", func() {
   125  				BeforeEach(func() {
   126  					meaningful := goa.NewErrorClass("goa-504-with-info", http.StatusGatewayTimeout)
   127  					h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
   128  						return meaningful("gatekeeper says no")
   129  					}
   130  				})
   131  
   132  				It("passes the response", func() {
   133  					var decoded errorResponse
   134  					Ω(rw.Status).Should(Equal(http.StatusGatewayTimeout))
   135  					Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
   136  					err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
   137  					Ω(err).ShouldNot(HaveOccurred())
   138  					Ω(decoded.Code).Should(Equal("goa-504-with-info"))
   139  					Ω(decoded.Detail).Should(Equal("gatekeeper says no"))
   140  				})
   141  			})
   142  		})
   143  	})
   144  
   145  	Context("with a handler returning a goa error", func() {
   146  		var gerr error
   147  
   148  		BeforeEach(func() {
   149  			service = newService(nil)
   150  			gerr = goa.NewErrorClass("code", 418)("teapot", "foobar", 42)
   151  			h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
   152  				return gerr
   153  			}
   154  		})
   155  
   156  		It("maps goa errors to HTTP responses", func() {
   157  			var decoded errorResponse
   158  			Ω(rw.Status).Should(Equal(gerr.(goa.ServiceError).ResponseStatus()))
   159  			Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
   160  			err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
   161  			Ω(err).ShouldNot(HaveOccurred())
   162  			Ω(decoded.Error()).Should(Equal(gerr.Error()))
   163  		})
   164  	})
   165  
   166  	Context("with a handler returning a pkg errors wrapped error", func() {
   167  		var wrappedError error
   168  		var logger *testLogger
   169  		verbose = true
   170  		BeforeEach(func() {
   171  			logger = new(testLogger)
   172  			service = newService(logger)
   173  			wrappedError = pErrors.Wrap(goa.ErrInternal("something crazy happened"), "an error")
   174  			h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
   175  				return wrappedError
   176  			}
   177  		})
   178  
   179  		It("maps pkg errors to HTTP responses", func() {
   180  			var decoded errorResponse
   181  			cause := pErrors.Cause(wrappedError)
   182  			Ω(rw.Status).Should(Equal(cause.(goa.ServiceError).ResponseStatus()))
   183  			Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
   184  			err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
   185  			Ω(err).ShouldNot(HaveOccurred())
   186  			Ω(decoded.Error()).Should(Equal(cause.Error()))
   187  		})
   188  		It("logs pkg errors stacktaces", func() {
   189  			var decoded errorResponse
   190  			err := service.Decoder.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
   191  			Ω(err).ShouldNot(HaveOccurred())
   192  			Ω(logger.ErrorEntries).Should(HaveLen(1))
   193  			data := logger.ErrorEntries[0].Data[1]
   194  			Ω(data).Should(ContainSubstring("error_handler_test.go"))
   195  		})
   196  	})
   197  })