From b1abd83f5b6de17e7317942c671802a83a459f8b Mon Sep 17 00:00:00 2001
From: Will McCutchen <will@mccutch.org>
Date: Sun, 18 Dec 2016 19:53:15 -0800
Subject: [PATCH] Add /delay/:n

---
 httpbin/handlers.go      | 24 ++++++++++++++++
 httpbin/handlers_test.go | 61 ++++++++++++++++++++++++++++++++++++++--
 httpbin/httpbin.go       |  6 +++-
 main.go                  |  4 ++-
 4 files changed, 91 insertions(+), 4 deletions(-)

diff --git a/httpbin/handlers.go b/httpbin/handlers.go
index c9255e4..9b5fa37 100644
--- a/httpbin/handlers.go
+++ b/httpbin/handlers.go
@@ -425,3 +425,27 @@ func (h *HTTPBin) Stream(w http.ResponseWriter, r *http.Request) {
 		f.Flush()
 	}
 }
+
+// Delay waits for n seconds before responding
+func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	if len(parts) != 3 {
+		http.Error(w, "Not found", http.StatusNotFound)
+		return
+	}
+	n, err := strconv.ParseFloat(parts[2], 64)
+	if err != nil {
+		http.Error(w, "Invalid float", http.StatusBadRequest)
+	}
+
+	// n is seconds as a float, and we allow down to millisecond resolution
+	delay := time.Duration(n*1000) * time.Millisecond
+	if delay > h.options.MaxResponseTime {
+		delay = h.options.MaxResponseTime
+	} else if delay < 0 {
+		delay = 0
+	}
+
+	<-time.After(delay)
+	h.RequestWithBody(w, r)
+}
diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go
index a9e7aea..88eeb33 100644
--- a/httpbin/handlers_test.go
+++ b/httpbin/handlers_test.go
@@ -19,10 +19,12 @@ import (
 	"time"
 )
 
-const maxMemory = 1024 * 1024
+const maxMemory int64 = 1024 * 1024
+const maxResponseTime time.Duration = 1 * time.Second
 
 var app = NewHTTPBin(&Options{
-	MaxMemory: maxMemory,
+	MaxMemory:       maxMemory,
+	MaxResponseTime: maxResponseTime,
 })
 
 var handler = app.Handler()
@@ -1225,3 +1227,58 @@ func TestStream(t *testing.T) {
 		})
 	}
 }
+
+func TestDelay(t *testing.T) {
+	var okTests = []struct {
+		url           string
+		expectedDelay time.Duration
+	}{
+		{"/delay/0", 0},
+		{"/delay/0.5", 500 * time.Millisecond},
+		{"/delay/1", maxResponseTime},
+		{"/delay/1.5", maxResponseTime},
+		{"/delay/-1", 0},
+		{"/delay/-3.14", 0},
+	}
+	for _, test := range okTests {
+		t.Run("ok"+test.url, func(t *testing.T) {
+			start := time.Now()
+
+			r, _ := http.NewRequest("GET", test.url, nil)
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, r)
+
+			elapsed := time.Now().Sub(start)
+
+			assertStatusCode(t, w, http.StatusOK)
+			assertHeader(t, w, "Content-Type", jsonContentType)
+
+			var resp *bodyResponse
+			err := json.Unmarshal(w.Body.Bytes(), &resp)
+			if err != nil {
+				t.Fatalf("error unmarshalling response: %s", err)
+			}
+
+			if elapsed < test.expectedDelay {
+				t.Fatalf("expected delay of %s, got %s", test.expectedDelay, elapsed)
+			}
+		})
+	}
+
+	var badTests = []struct {
+		url  string
+		code int
+	}{
+		{"/delay", http.StatusNotFound},
+		{"/delay/foo", http.StatusBadRequest},
+	}
+
+	for _, test := range badTests {
+		t.Run("bad"+test.url, func(t *testing.T) {
+			r, _ := http.NewRequest("GET", test.url, nil)
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, r)
+			assertStatusCode(t, w, test.code)
+		})
+	}
+}
diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go
index 5de8208..04cad81 100644
--- a/httpbin/httpbin.go
+++ b/httpbin/httpbin.go
@@ -3,6 +3,7 @@ package httpbin
 import (
 	"net/http"
 	"net/url"
+	"time"
 )
 
 const jsonContentType = "application/json; encoding=utf-8"
@@ -71,7 +72,8 @@ type streamResponse struct {
 
 // Options are used to configure HTTPBin
 type Options struct {
-	MaxMemory int64
+	MaxMemory       int64
+	MaxResponseTime time.Duration
 }
 
 // HTTPBin contains the business logic
@@ -115,12 +117,14 @@ func (h *HTTPBin) Handler() http.Handler {
 	mux.HandleFunc("/gzip", h.Gzip)
 
 	mux.HandleFunc("/stream/", h.Stream)
+	mux.HandleFunc("/delay/", h.Delay)
 
 	// Make sure our ServeMux doesn't "helpfully" redirect these invalid
 	// endpoints by adding a trailing slash. See the ServeMux docs for more
 	// info: https://golang.org/pkg/net/http/#ServeMux
 	mux.HandleFunc("/absolute-redirect", http.NotFound)
 	mux.HandleFunc("/basic-auth", http.NotFound)
+	mux.HandleFunc("/delay", http.NotFound)
 	mux.HandleFunc("/digest-auth", http.NotFound)
 	mux.HandleFunc("/hidden-basic-auth", http.NotFound)
 	mux.HandleFunc("/redirect", http.NotFound)
diff --git a/main.go b/main.go
index e4f8310..47c9d90 100644
--- a/main.go
+++ b/main.go
@@ -3,13 +3,15 @@ package main
 import (
 	"log"
 	"net/http"
+	"time"
 
 	"github.com/mccutchen/go-httpbin/httpbin"
 )
 
 func main() {
 	h := httpbin.NewHTTPBin(&httpbin.Options{
-		MaxMemory: 1024 * 1024 * 5,
+		MaxMemory:       1024 * 1024 * 5,
+		MaxResponseTime: 10 * time.Second,
 	})
 	log.Printf("listening on 9999")
 	err := http.ListenAndServe(":9999", h.Handler())
-- 
GitLab