diff options
Diffstat (limited to 'vendor/github.com/onsi/gomega/ghttp/test_server.go')
-rw-r--r-- | vendor/github.com/onsi/gomega/ghttp/test_server.go | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/vendor/github.com/onsi/gomega/ghttp/test_server.go b/vendor/github.com/onsi/gomega/ghttp/test_server.go new file mode 100644 index 0000000..40d92de --- /dev/null +++ b/vendor/github.com/onsi/gomega/ghttp/test_server.go @@ -0,0 +1,381 @@ +/* +Package ghttp supports testing HTTP clients by providing a test server (simply a thin wrapper around httptest's server) that supports +registering multiple handlers. Incoming requests are not routed between the different handlers +- rather it is merely the order of the handlers that matters. The first request is handled by the first +registered handler, the second request by the second handler, etc. + +The intent here is to have each handler *verify* that the incoming request is valid. To accomplish, ghttp +also provides a collection of bite-size handlers that each perform one aspect of request verification. These can +be composed together and registered with a ghttp server. The result is an expressive language for describing +the requests generated by the client under test. + +Here's a simple example, note that the server handler is only defined in one BeforeEach and then modified, as required, by the nested BeforeEaches. +A more comprehensive example is available at https://onsi.github.io/gomega/#_testing_http_clients + + var _ = Describe("A Sprockets Client", func() { + var server *ghttp.Server + var client *SprocketClient + BeforeEach(func() { + server = ghttp.NewServer() + client = NewSprocketClient(server.URL(), "skywalker", "tk427") + }) + + AfterEach(func() { + server.Close() + }) + + Describe("fetching sprockets", func() { + var statusCode int + var sprockets []Sprocket + BeforeEach(func() { + statusCode = http.StatusOK + sprockets = []Sprocket{} + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/sprockets"), + ghttp.VerifyBasicAuth("skywalker", "tk427"), + ghttp.RespondWithJSONEncodedPtr(&statusCode, &sprockets), + )) + }) + + Context("when requesting all sprockets", func() { + Context("when the response is succesful", func() { + BeforeEach(func() { + sprockets = []Sprocket{ + NewSprocket("Alfalfa"), + NewSprocket("Banana"), + } + }) + + It("should return the returned sprockets", func() { + Ω(client.Sprockets()).Should(Equal(sprockets)) + }) + }) + + Context("when the response is missing", func() { + BeforeEach(func() { + statusCode = http.StatusNotFound + }) + + It("should return an empty list of sprockets", func() { + Ω(client.Sprockets()).Should(BeEmpty()) + }) + }) + + Context("when the response fails to authenticate", func() { + BeforeEach(func() { + statusCode = http.StatusUnauthorized + }) + + It("should return an AuthenticationError error", func() { + sprockets, err := client.Sprockets() + Ω(sprockets).Should(BeEmpty()) + Ω(err).Should(MatchError(AuthenticationError)) + }) + }) + + Context("when the response is a server failure", func() { + BeforeEach(func() { + statusCode = http.StatusInternalServerError + }) + + It("should return an InternalError error", func() { + sprockets, err := client.Sprockets() + Ω(sprockets).Should(BeEmpty()) + Ω(err).Should(MatchError(InternalError)) + }) + }) + }) + + Context("when requesting some sprockets", func() { + BeforeEach(func() { + sprockets = []Sprocket{ + NewSprocket("Alfalfa"), + NewSprocket("Banana"), + } + + server.WrapHandler(0, ghttp.VerifyRequest("GET", "/sprockets", "filter=FOOD")) + }) + + It("should make the request with a filter", func() { + Ω(client.Sprockets("food")).Should(Equal(sprockets)) + }) + }) + }) + }) +*/ +package ghttp + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "regexp" + "strings" + "sync" + + . "github.com/onsi/gomega" +) + +func new() *Server { + return &Server{ + AllowUnhandledRequests: false, + UnhandledRequestStatusCode: http.StatusInternalServerError, + writeLock: &sync.Mutex{}, + } +} + +type routedHandler struct { + method string + pathRegexp *regexp.Regexp + path string + handler http.HandlerFunc +} + +// NewServer returns a new `*ghttp.Server` that wraps an `httptest` server. The server is started automatically. +func NewServer() *Server { + s := new() + s.HTTPTestServer = httptest.NewServer(s) + return s +} + +// NewUnstartedServer return a new, unstarted, `*ghttp.Server`. Useful for specifying a custom listener on `server.HTTPTestServer`. +func NewUnstartedServer() *Server { + s := new() + s.HTTPTestServer = httptest.NewUnstartedServer(s) + return s +} + +// NewTLSServer returns a new `*ghttp.Server` that wraps an `httptest` TLS server. The server is started automatically. +func NewTLSServer() *Server { + s := new() + s.HTTPTestServer = httptest.NewTLSServer(s) + return s +} + +type Server struct { + //The underlying httptest server + HTTPTestServer *httptest.Server + + //Defaults to false. If set to true, the Server will allow more requests than there are registered handlers. + AllowUnhandledRequests bool + + //The status code returned when receiving an unhandled request. + //Defaults to http.StatusInternalServerError. + //Only applies if AllowUnhandledRequests is true + UnhandledRequestStatusCode int + + //If provided, ghttp will log about each request received to the provided io.Writer + //Defaults to nil + //If you're using Ginkgo, set this to GinkgoWriter to get improved output during failures + Writer io.Writer + + receivedRequests []*http.Request + requestHandlers []http.HandlerFunc + routedHandlers []routedHandler + + writeLock *sync.Mutex + calls int +} + +//Start() starts an unstarted ghttp server. It is a catastrophic error to call Start more than once (thanks, httptest). +func (s *Server) Start() { + s.HTTPTestServer.Start() +} + +//URL() returns a url that will hit the server +func (s *Server) URL() string { + return s.HTTPTestServer.URL +} + +//Addr() returns the address on which the server is listening. +func (s *Server) Addr() string { + return s.HTTPTestServer.Listener.Addr().String() +} + +//Close() should be called at the end of each test. It spins down and cleans up the test server. +func (s *Server) Close() { + s.writeLock.Lock() + server := s.HTTPTestServer + s.HTTPTestServer = nil + s.writeLock.Unlock() + + if server != nil { + server.Close() + } +} + +//ServeHTTP() makes Server an http.Handler +//When the server receives a request it handles the request in the following order: +// +//1. If the request matches a handler registered with RouteToHandler, that handler is called. +//2. Otherwise, if there are handlers registered via AppendHandlers, those handlers are called in order. +//3. If all registered handlers have been called then: +// a) If AllowUnhandledRequests is true, the request will be handled with response code of UnhandledRequestStatusCode +// b) If AllowUnhandledRequests is false, the request will not be handled and the current test will be marked as failed. +func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.writeLock.Lock() + defer func() { + e := recover() + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + } + + //If the handler panics GHTTP will silently succeed. This is bad™. + //To catch this case we need to fail the test if the handler has panicked. + //However, if the handler is panicking because Ginkgo's causing it to panic (i.e. an assertion failed) + //then we shouldn't double-report the error as this will confuse people. + + //So: step 1, if this is a Ginkgo panic - do nothing, Ginkgo's aware of the failure + eAsString, ok := e.(string) + if ok && strings.Contains(eAsString, "defer GinkgoRecover()") { + return + } + + //If we're here, we have to do step 2: assert that the error is nil. This assertion will + //allow us to fail the test suite (note: we can't call Fail since Gomega is not allowed to import Ginkgo). + //Since a failed assertion throws a panic, and we are likely in a goroutine, we need to defer within our defer! + defer func() { + recover() + }() + Ω(e).Should(BeNil(), "Handler Panicked") + }() + + if s.Writer != nil { + s.Writer.Write([]byte(fmt.Sprintf("GHTTP Received Request: %s - %s\n", req.Method, req.URL))) + } + + s.receivedRequests = append(s.receivedRequests, req) + if routedHandler, ok := s.handlerForRoute(req.Method, req.URL.Path); ok { + s.writeLock.Unlock() + routedHandler(w, req) + } else if s.calls < len(s.requestHandlers) { + h := s.requestHandlers[s.calls] + s.calls++ + s.writeLock.Unlock() + h(w, req) + } else { + s.writeLock.Unlock() + if s.AllowUnhandledRequests { + ioutil.ReadAll(req.Body) + req.Body.Close() + w.WriteHeader(s.UnhandledRequestStatusCode) + } else { + Ω(req).Should(BeNil(), "Received Unhandled Request") + } + } +} + +//ReceivedRequests is an array containing all requests received by the server (both handled and unhandled requests) +func (s *Server) ReceivedRequests() []*http.Request { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + return s.receivedRequests +} + +//RouteToHandler can be used to register handlers that will always handle requests that match +//the passed in method and path. +// +//The path may be either a string object or a *regexp.Regexp. +func (s *Server) RouteToHandler(method string, path interface{}, handler http.HandlerFunc) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + rh := routedHandler{ + method: method, + handler: handler, + } + + switch p := path.(type) { + case *regexp.Regexp: + rh.pathRegexp = p + case string: + rh.path = p + default: + panic("path must be a string or a regular expression") + } + + for i, existingRH := range s.routedHandlers { + if existingRH.method == method && + reflect.DeepEqual(existingRH.pathRegexp, rh.pathRegexp) && + existingRH.path == rh.path { + s.routedHandlers[i] = rh + return + } + } + s.routedHandlers = append(s.routedHandlers, rh) +} + +func (s *Server) handlerForRoute(method string, path string) (http.HandlerFunc, bool) { + for _, rh := range s.routedHandlers { + if rh.method == method { + if rh.pathRegexp != nil { + if rh.pathRegexp.Match([]byte(path)) { + return rh.handler, true + } + } else if rh.path == path { + return rh.handler, true + } + } + } + + return nil, false +} + +//AppendHandlers will appends http.HandlerFuncs to the server's list of registered handlers. The first incoming request is handled by the first handler, the second by the second, etc... +func (s *Server) AppendHandlers(handlers ...http.HandlerFunc) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.requestHandlers = append(s.requestHandlers, handlers...) +} + +//SetHandler overrides the registered handler at the passed in index with the passed in handler +//This is useful, for example, when a server has been set up in a shared context, but must be tweaked +//for a particular test. +func (s *Server) SetHandler(index int, handler http.HandlerFunc) { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.requestHandlers[index] = handler +} + +//GetHandler returns the handler registered at the passed in index. +func (s *Server) GetHandler(index int) http.HandlerFunc { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + return s.requestHandlers[index] +} + +func (s *Server) Reset() { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.HTTPTestServer.CloseClientConnections() + s.calls = 0 + s.receivedRequests = nil + s.requestHandlers = nil + s.routedHandlers = nil +} + +//WrapHandler combines the passed in handler with the handler registered at the passed in index. +//This is useful, for example, when a server has been set up in a shared context but must be tweaked +//for a particular test. +// +//If the currently registered handler is A, and the new passed in handler is B then +//WrapHandler will generate a new handler that first calls A, then calls B, and assign it to index +func (s *Server) WrapHandler(index int, handler http.HandlerFunc) { + existingHandler := s.GetHandler(index) + s.SetHandler(index, CombineHandlers(existingHandler, handler)) +} + +func (s *Server) CloseClientConnections() { + s.writeLock.Lock() + defer s.writeLock.Unlock() + + s.HTTPTestServer.CloseClientConnections() +} |