2 Package ghttp supports testing HTTP clients by providing a test server (simply a thin wrapper around httptest's server) that supports
3 registering multiple handlers. Incoming requests are not routed between the different handlers
4 - rather it is merely the order of the handlers that matters. The first request is handled by the first
5 registered handler, the second request by the second handler, etc.
7 The intent here is to have each handler *verify* that the incoming request is valid. To accomplish, ghttp
8 also provides a collection of bite-size handlers that each perform one aspect of request verification. These can
9 be composed together and registered with a ghttp server. The result is an expressive language for describing
10 the requests generated by the client under test.
12 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.
13 A more comprehensive example is available at https://onsi.github.io/gomega/#_testing_http_clients
15 var _ = Describe("A Sprockets Client", func() {
16 var server *ghttp.Server
17 var client *SprocketClient
19 server = ghttp.NewServer()
20 client = NewSprocketClient(server.URL(), "skywalker", "tk427")
27 Describe("fetching sprockets", func() {
29 var sprockets []Sprocket
31 statusCode = http.StatusOK
32 sprockets = []Sprocket{}
33 server.AppendHandlers(ghttp.CombineHandlers(
34 ghttp.VerifyRequest("GET", "/sprockets"),
35 ghttp.VerifyBasicAuth("skywalker", "tk427"),
36 ghttp.RespondWithJSONEncodedPtr(&statusCode, &sprockets),
40 Context("when requesting all sprockets", func() {
41 Context("when the response is succesful", func() {
43 sprockets = []Sprocket{
44 NewSprocket("Alfalfa"),
45 NewSprocket("Banana"),
49 It("should return the returned sprockets", func() {
50 Ω(client.Sprockets()).Should(Equal(sprockets))
54 Context("when the response is missing", func() {
56 statusCode = http.StatusNotFound
59 It("should return an empty list of sprockets", func() {
60 Ω(client.Sprockets()).Should(BeEmpty())
64 Context("when the response fails to authenticate", func() {
66 statusCode = http.StatusUnauthorized
69 It("should return an AuthenticationError error", func() {
70 sprockets, err := client.Sprockets()
71 Ω(sprockets).Should(BeEmpty())
72 Ω(err).Should(MatchError(AuthenticationError))
76 Context("when the response is a server failure", func() {
78 statusCode = http.StatusInternalServerError
81 It("should return an InternalError error", func() {
82 sprockets, err := client.Sprockets()
83 Ω(sprockets).Should(BeEmpty())
84 Ω(err).Should(MatchError(InternalError))
89 Context("when requesting some sprockets", func() {
91 sprockets = []Sprocket{
92 NewSprocket("Alfalfa"),
93 NewSprocket("Banana"),
96 server.WrapHandler(0, ghttp.VerifyRequest("GET", "/sprockets", "filter=FOOD"))
99 It("should make the request with a filter", func() {
100 Ω(client.Sprockets("food")).Should(Equal(sprockets))
119 . "github.com/onsi/gomega"
124 AllowUnhandledRequests: false,
125 UnhandledRequestStatusCode: http.StatusInternalServerError,
126 writeLock: &sync.Mutex{},
130 type routedHandler struct {
132 pathRegexp *regexp.Regexp
134 handler http.HandlerFunc
137 // NewServer returns a new `*ghttp.Server` that wraps an `httptest` server. The server is started automatically.
138 func NewServer() *Server {
140 s.HTTPTestServer = httptest.NewServer(s)
144 // NewUnstartedServer return a new, unstarted, `*ghttp.Server`. Useful for specifying a custom listener on `server.HTTPTestServer`.
145 func NewUnstartedServer() *Server {
147 s.HTTPTestServer = httptest.NewUnstartedServer(s)
151 // NewTLSServer returns a new `*ghttp.Server` that wraps an `httptest` TLS server. The server is started automatically.
152 func NewTLSServer() *Server {
154 s.HTTPTestServer = httptest.NewTLSServer(s)
159 //The underlying httptest server
160 HTTPTestServer *httptest.Server
162 //Defaults to false. If set to true, the Server will allow more requests than there are registered handlers.
163 AllowUnhandledRequests bool
165 //The status code returned when receiving an unhandled request.
166 //Defaults to http.StatusInternalServerError.
167 //Only applies if AllowUnhandledRequests is true
168 UnhandledRequestStatusCode int
170 //If provided, ghttp will log about each request received to the provided io.Writer
172 //If you're using Ginkgo, set this to GinkgoWriter to get improved output during failures
175 receivedRequests []*http.Request
176 requestHandlers []http.HandlerFunc
177 routedHandlers []routedHandler
179 writeLock *sync.Mutex
183 //Start() starts an unstarted ghttp server. It is a catastrophic error to call Start more than once (thanks, httptest).
184 func (s *Server) Start() {
185 s.HTTPTestServer.Start()
188 //URL() returns a url that will hit the server
189 func (s *Server) URL() string {
190 return s.HTTPTestServer.URL
193 //Addr() returns the address on which the server is listening.
194 func (s *Server) Addr() string {
195 return s.HTTPTestServer.Listener.Addr().String()
198 //Close() should be called at the end of each test. It spins down and cleans up the test server.
199 func (s *Server) Close() {
201 server := s.HTTPTestServer
202 s.HTTPTestServer = nil
210 //ServeHTTP() makes Server an http.Handler
211 //When the server receives a request it handles the request in the following order:
213 //1. If the request matches a handler registered with RouteToHandler, that handler is called.
214 //2. Otherwise, if there are handlers registered via AppendHandlers, those handlers are called in order.
215 //3. If all registered handlers have been called then:
216 // a) If AllowUnhandledRequests is true, the request will be handled with response code of UnhandledRequestStatusCode
217 // b) If AllowUnhandledRequests is false, the request will not be handled and the current test will be marked as failed.
218 func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
223 w.WriteHeader(http.StatusInternalServerError)
226 //If the handler panics GHTTP will silently succeed. This is bad™.
227 //To catch this case we need to fail the test if the handler has panicked.
228 //However, if the handler is panicking because Ginkgo's causing it to panic (i.e. an assertion failed)
229 //then we shouldn't double-report the error as this will confuse people.
231 //So: step 1, if this is a Ginkgo panic - do nothing, Ginkgo's aware of the failure
232 eAsString, ok := e.(string)
233 if ok && strings.Contains(eAsString, "defer GinkgoRecover()") {
237 //If we're here, we have to do step 2: assert that the error is nil. This assertion will
238 //allow us to fail the test suite (note: we can't call Fail since Gomega is not allowed to import Ginkgo).
239 //Since a failed assertion throws a panic, and we are likely in a goroutine, we need to defer within our defer!
243 Ω(e).Should(BeNil(), "Handler Panicked")
247 s.Writer.Write([]byte(fmt.Sprintf("GHTTP Received Request: %s - %s\n", req.Method, req.URL)))
250 s.receivedRequests = append(s.receivedRequests, req)
251 if routedHandler, ok := s.handlerForRoute(req.Method, req.URL.Path); ok {
253 routedHandler(w, req)
254 } else if s.calls < len(s.requestHandlers) {
255 h := s.requestHandlers[s.calls]
261 if s.AllowUnhandledRequests {
262 ioutil.ReadAll(req.Body)
264 w.WriteHeader(s.UnhandledRequestStatusCode)
266 Ω(req).Should(BeNil(), "Received Unhandled Request")
271 //ReceivedRequests is an array containing all requests received by the server (both handled and unhandled requests)
272 func (s *Server) ReceivedRequests() []*http.Request {
274 defer s.writeLock.Unlock()
276 return s.receivedRequests
279 //RouteToHandler can be used to register handlers that will always handle requests that match
280 //the passed in method and path.
282 //The path may be either a string object or a *regexp.Regexp.
283 func (s *Server) RouteToHandler(method string, path interface{}, handler http.HandlerFunc) {
285 defer s.writeLock.Unlock()
292 switch p := path.(type) {
298 panic("path must be a string or a regular expression")
301 for i, existingRH := range s.routedHandlers {
302 if existingRH.method == method &&
303 reflect.DeepEqual(existingRH.pathRegexp, rh.pathRegexp) &&
304 existingRH.path == rh.path {
305 s.routedHandlers[i] = rh
309 s.routedHandlers = append(s.routedHandlers, rh)
312 func (s *Server) handlerForRoute(method string, path string) (http.HandlerFunc, bool) {
313 for _, rh := range s.routedHandlers {
314 if rh.method == method {
315 if rh.pathRegexp != nil {
316 if rh.pathRegexp.Match([]byte(path)) {
317 return rh.handler, true
319 } else if rh.path == path {
320 return rh.handler, true
328 //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...
329 func (s *Server) AppendHandlers(handlers ...http.HandlerFunc) {
331 defer s.writeLock.Unlock()
333 s.requestHandlers = append(s.requestHandlers, handlers...)
336 //SetHandler overrides the registered handler at the passed in index with the passed in handler
337 //This is useful, for example, when a server has been set up in a shared context, but must be tweaked
338 //for a particular test.
339 func (s *Server) SetHandler(index int, handler http.HandlerFunc) {
341 defer s.writeLock.Unlock()
343 s.requestHandlers[index] = handler
346 //GetHandler returns the handler registered at the passed in index.
347 func (s *Server) GetHandler(index int) http.HandlerFunc {
349 defer s.writeLock.Unlock()
351 return s.requestHandlers[index]
354 func (s *Server) Reset() {
356 defer s.writeLock.Unlock()
358 s.HTTPTestServer.CloseClientConnections()
360 s.receivedRequests = nil
361 s.requestHandlers = nil
362 s.routedHandlers = nil
365 //WrapHandler combines the passed in handler with the handler registered at the passed in index.
366 //This is useful, for example, when a server has been set up in a shared context but must be tweaked
367 //for a particular test.
369 //If the currently registered handler is A, and the new passed in handler is B then
370 //WrapHandler will generate a new handler that first calls A, then calls B, and assign it to index
371 func (s *Server) WrapHandler(index int, handler http.HandlerFunc) {
372 existingHandler := s.GetHandler(index)
373 s.SetHandler(index, CombineHandlers(existingHandler, handler))
376 func (s *Server) CloseClientConnections() {
378 defer s.writeLock.Unlock()
380 s.HTTPTestServer.CloseClientConnections()