From 8f99a5140a620438fec66e0cfcdc6561e7472220 Mon Sep 17 00:00:00 2001
From: Will McCutchen <will@mccutch.org>
Date: Mon, 29 May 2017 19:19:32 -0700
Subject: [PATCH] Add /links

---
 httpbin/handlers.go      | 46 +++++++++++++++++++++++++++
 httpbin/handlers_test.go | 69 ++++++++++++++++++++++++++++++++++++++++
 httpbin/httpbin.go       |  3 ++
 3 files changed, 118 insertions(+)

diff --git a/httpbin/handlers.go b/httpbin/handlers.go
index 261c460..6bec584 100644
--- a/httpbin/handlers.go
+++ b/httpbin/handlers.go
@@ -732,3 +732,49 @@ func handleBytes(w http.ResponseWriter, r *http.Request, streaming bool) {
 		write(chunk)
 	}
 }
+
+// Links redirects to the first page in a series of N links
+func (h *HTTPBin) Links(w http.ResponseWriter, r *http.Request) {
+	parts := strings.Split(r.URL.Path, "/")
+	if len(parts) != 3 && len(parts) != 4 {
+		http.Error(w, "Not found", http.StatusNotFound)
+		return
+	}
+
+	n, err := strconv.Atoi(parts[2])
+	if err != nil || n < 0 || n > 256 {
+		http.Error(w, "Invalid link count", http.StatusBadRequest)
+		return
+	}
+
+	// Are we handling /links/<n>/<offset>? If so, render an HTML page
+	if len(parts) == 4 {
+		offset, err := strconv.Atoi(parts[3])
+		if err != nil {
+			http.Error(w, "Invalid offset", http.StatusBadRequest)
+		}
+		doLinksPage(w, r, n, offset)
+		return
+	}
+
+	// Otherwise, redirect from /links/<n> to /links/<n>/0
+	r.URL.Path = r.URL.Path + "/0"
+	w.Header().Set("Location", r.URL.String())
+	w.WriteHeader(http.StatusFound)
+}
+
+// doLinksPage renders a page with a series of N links
+func doLinksPage(w http.ResponseWriter, r *http.Request, n int, offset int) {
+	w.Header().Add("Content-Type", htmlContentType)
+	w.WriteHeader(http.StatusOK)
+
+	w.Write([]byte("<html><head><title>Links</title></head><body>"))
+	for i := 0; i < n; i++ {
+		if i == offset {
+			fmt.Fprintf(w, "%d ", i)
+		} else {
+			fmt.Fprintf(w, `<a href="/links/%d/%d">%d</a> `, n, i, i)
+		}
+	}
+	w.Write([]byte("</body></html>"))
+}
diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go
index 824cb6b..7b8bf2f 100644
--- a/httpbin/handlers_test.go
+++ b/httpbin/handlers_test.go
@@ -1833,3 +1833,72 @@ func TestStreamBytes(t *testing.T) {
 		})
 	}
 }
+
+func TestLinks(t *testing.T) {
+	var redirectTests = []struct {
+		url              string
+		expectedLocation string
+	}{
+		{"/links/1", "/links/1/0"},
+		{"/links/100", "/links/100/0"},
+	}
+
+	for _, test := range redirectTests {
+		t.Run("ok"+test.url, func(t *testing.T) {
+			r, _ := http.NewRequest("GET", test.url, nil)
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, r)
+
+			assertStatusCode(t, w, http.StatusFound)
+			assertHeader(t, w, "Location", test.expectedLocation)
+		})
+	}
+
+	var errorTests = []struct {
+		url            string
+		expectedStatus int
+	}{
+		// invalid N
+		{"/links/3.14", http.StatusBadRequest},
+		{"/links/-1", http.StatusBadRequest},
+		{"/links/257", http.StatusBadRequest},
+
+		// invalid offset
+		{"/links/1/3.14", http.StatusBadRequest},
+		{"/links/1/foo", http.StatusBadRequest},
+	}
+
+	for _, test := range errorTests {
+		t.Run("error"+test.url, func(t *testing.T) {
+			r, _ := http.NewRequest("GET", test.url, nil)
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, r)
+
+			assertStatusCode(t, w, test.expectedStatus)
+		})
+	}
+
+	var linksPageTests = []struct {
+		url             string
+		expectedContent string
+	}{
+		{"/links/2/0", `<html><head><title>Links</title></head><body>0 <a href="/links/2/1">1</a> </body></html>`},
+		{"/links/2/1", `<html><head><title>Links</title></head><body><a href="/links/2/0">0</a> 1 </body></html>`},
+
+		// offsets too large and too small are ignored
+		{"/links/2/2", `<html><head><title>Links</title></head><body><a href="/links/2/0">0</a> <a href="/links/2/1">1</a> </body></html>`},
+		{"/links/2/10", `<html><head><title>Links</title></head><body><a href="/links/2/0">0</a> <a href="/links/2/1">1</a> </body></html>`},
+		{"/links/2/-1", `<html><head><title>Links</title></head><body><a href="/links/2/0">0</a> <a href="/links/2/1">1</a> </body></html>`},
+	}
+	for _, test := range linksPageTests {
+		t.Run("ok"+test.url, func(t *testing.T) {
+			r, _ := http.NewRequest("GET", test.url, nil)
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, r)
+
+			assertStatusCode(t, w, http.StatusOK)
+			assertContentType(t, w, htmlContentType)
+			assertBodyEquals(t, w, test.expectedContent)
+		})
+	}
+}
diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go
index 9cabf8d..81460f1 100644
--- a/httpbin/httpbin.go
+++ b/httpbin/httpbin.go
@@ -133,6 +133,8 @@ func (h *HTTPBin) Handler() http.Handler {
 	mux.HandleFunc("/cache/", h.CacheControl)
 	mux.HandleFunc("/etag/", h.ETag)
 
+	mux.HandleFunc("/links/", h.Links)
+
 	// 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
@@ -147,6 +149,7 @@ func (h *HTTPBin) Handler() http.Handler {
 	mux.HandleFunc("/stream", http.NotFound)
 	mux.HandleFunc("/bytes", http.NotFound)
 	mux.HandleFunc("/stream-bytes", http.NotFound)
+	mux.HandleFunc("/links", http.NotFound)
 
 	return logger(cors(mux))
 }
-- 
GitLab