diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 4c033cdd6c13f336a1daa76480a3737c996b250a..baccdfa3b0a06aee27554564cbb73ca0c9e3d8d6 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -5,8 +5,18 @@ import ( "fmt" "net/http" "net/url" + "strconv" + "strings" ) +var acceptedMediaTypes = []string{ + "image/webp", + "image/svg+xml", + "image/jpeg", + "image/png", + "image/", +} + // Index renders an HTML index page func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) { w.Write(MustAsset("index.html")) @@ -87,3 +97,82 @@ func (h *HTTPBin) Headers(w http.ResponseWriter, r *http.Request) { }) writeJSON(w, body, http.StatusOK) } + +// Status responds with the specified status code. TODO: support random choice +// from multiple, optionally weighted status codes. +func (h *HTTPBin) Status(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + if len(parts) != 3 { + http.Error(w, "Not found", http.StatusNotFound) + return + } + code, err := strconv.Atoi(parts[2]) + if err != nil { + http.Error(w, "Invalid status", http.StatusBadRequest) + } + + type statusCase struct { + headers map[string]string + body []byte + } + + redirectHeaders := &statusCase{ + headers: map[string]string{ + "Location": "/redirect/1", + }, + } + notAcceptableBody, _ := json.Marshal(map[string]interface{}{ + "message": "Client did not request a supported media type", + "accept": acceptedMediaTypes, + }) + + specialCases := map[int]*statusCase{ + 301: redirectHeaders, + 302: redirectHeaders, + 303: redirectHeaders, + 305: redirectHeaders, + 307: redirectHeaders, + 401: &statusCase{ + headers: map[string]string{ + "WWW-Authenticate": `Basic realm="Fake Realm"`, + }, + }, + 402: &statusCase{ + body: []byte("Fuck you, pay me!"), + headers: map[string]string{ + "X-More-Info": "http://vimeo.com/22053820", + }, + }, + 406: &statusCase{ + body: notAcceptableBody, + headers: map[string]string{ + "Content-Type": "application/json; encoding=utf-8", + }, + }, + 407: &statusCase{ + headers: map[string]string{ + "Proxy-Authenticate": `Basic realm="Fake Realm"`, + }, + }, + 418: &statusCase{ + body: []byte("I'm a teapot!"), + headers: map[string]string{ + "X-More-Info": "http://tools.ietf.org/html/rfc2324", + }, + }, + } + + if specialCase, ok := specialCases[code]; ok { + if specialCase.headers != nil { + for key, val := range specialCase.headers { + w.Header().Set(key, val) + } + } + w.WriteHeader(code) + if specialCase.body != nil { + w.Write(specialCase.body) + } + } else { + w.WriteHeader(code) + } +} diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index c4450e42803eb6e5e0a0f95210ff32ca99ca9cff..11bc1ffed6cffaa284d4c4676cfe5b286e91cc9f 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -3,6 +3,7 @@ package httpbin import ( "bytes" "encoding/json" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -25,12 +26,16 @@ func assertStatusCode(t *testing.T, w *httptest.ResponseRecorder, code int) { } } -func assertContentType(t *testing.T, w *httptest.ResponseRecorder, contentType string) { - if w.Header().Get("Content-Type") != contentType { - t.Fatalf("expected content type %s, got %s", contentType, w.Header().Get("Content-Type")) +func assertHeader(t *testing.T, w *httptest.ResponseRecorder, key, val string) { + if w.Header().Get(key) != val { + t.Fatalf("expected header %s=%#v, got %#v", key, val, w.Header().Get(key)) } } +func assertContentType(t *testing.T, w *httptest.ResponseRecorder, contentType string) { + assertHeader(t, w, "Content-Type", contentType) +} + func assertBodyContains(t *testing.T, w *httptest.ResponseRecorder, needle string) { if !strings.Contains(w.Body.String(), needle) { t.Fatalf("expected string %v in body", needle) @@ -118,9 +123,7 @@ func TestGet__CORSHeadersWithoutRequestOrigin(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, r) - if w.Header().Get("Access-Control-Allow-Origin") != "*" { - t.Fatalf("expected Access-Control-Allow-Origin=*, got %#v", w.Header().Get("Access-Control-Allow-Origin")) - } + assertHeader(t, w, "Access-Control-Allow-Origin", "*") } func TestGet__CORSHeadersWithRequestOrigin(t *testing.T) { @@ -129,9 +132,7 @@ func TestGet__CORSHeadersWithRequestOrigin(t *testing.T) { w := httptest.NewRecorder() handler.ServeHTTP(w, r) - if w.Header().Get("Access-Control-Allow-Origin") != "origin" { - t.Fatalf("expected Access-Control-Allow-Origin=origin, got %#v", w.Header().Get("Access-Control-Allow-Origin")) - } + assertHeader(t, w, "Access-Control-Allow-Origin", "origin") } func TestGet__CORSHeadersWithOptionsVerb(t *testing.T) { @@ -150,9 +151,7 @@ func TestGet__CORSHeadersWithOptionsVerb(t *testing.T) { {"Access-Control-Allow-Headers", ""}, } for _, test := range headerTests { - if w.Header().Get(test.key) != test.expected { - t.Fatalf("expected %s = %#v, got %#v", test.key, test.expected, w.Header().Get(test.key)) - } + assertHeader(t, w, test.key, test.expected) } } @@ -169,9 +168,7 @@ func TestGet__CORSAllowHeaders(t *testing.T) { {"Access-Control-Allow-Headers", "X-Test-Header"}, } for _, test := range headerTests { - if w.Header().Get(test.key) != test.expected { - t.Fatalf("expected %s = %#v, got %#v", test.key, test.expected, w.Header().Get(test.key)) - } + assertHeader(t, w, test.key, test.expected) } } @@ -424,3 +421,43 @@ func TestPost__BodyTooBig(t *testing.T) { assertStatusCode(t, w, http.StatusBadRequest) assertContentType(t, w, "application/json; encoding=utf-8") } + +func TestStatus__Simple(t *testing.T) { + redirectHeaders := map[string]string{ + "Location": "/redirect/1", + } + unauthorizedHeaders := map[string]string{ + "WWW-Authenticate": `Basic realm="Fake Realm"`, + } + var tests = []struct { + code int + headers map[string]string + body string + }{ + {200, nil, ""}, + {301, redirectHeaders, ""}, + {302, redirectHeaders, ""}, + {401, unauthorizedHeaders, ""}, + {418, nil, "I'm a teapot!"}, + } + + for _, test := range tests { + r, _ := http.NewRequest("GET", fmt.Sprintf("/status/%d", test.code), nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, test.code) + + if test.headers != nil { + for key, val := range test.headers { + assertHeader(t, w, key, val) + } + } + + if test.body != "" { + if w.Body.String() != test.body { + t.Fatalf("expected body %#v, got %#v", test.body, w.Body.String()) + } + } + } +} diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index b7a8b396e2e420b3bfd3bc29de1f9cc039c2125e..83a6d26e62b5de3ae7818c0b27744c8d7cb57874 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -64,6 +64,7 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/ip", h.IP) mux.HandleFunc("/user-agent", h.UserAgent) mux.HandleFunc("/headers", h.Headers) + mux.HandleFunc("/status/", h.Status) return logger(cors(mux)) }