diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 46a9b647d2cf716cb153597838702c8fab7474ee..25fd90073d872befe8194f6d76dd32b57481e677 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "time" ) var acceptedMediaTypes = []string{ @@ -246,10 +247,7 @@ func (h *HTTPBin) AbsoluteRedirect(w http.ResponseWriter, r *http.Request) { func (h *HTTPBin) Cookies(w http.ResponseWriter, r *http.Request) { resp := cookiesResponse{} for _, c := range r.Cookies() { - if _, found := resp[c.Name]; !found { - resp[c.Name] = []string{} - } - resp[c.Name] = append(resp[c.Name], c.Value) + resp[c.Name] = c.Value } body, _ := json.Marshal(resp) writeJSON(w, body, http.StatusOK) @@ -259,15 +257,29 @@ func (h *HTTPBin) Cookies(w http.ResponseWriter, r *http.Request) { // Cookies endpoint func (h *HTTPBin) SetCookies(w http.ResponseWriter, r *http.Request) { params := r.URL.Query() - for k, vs := range params { - for _, v := range vs { - fmt.Printf("setting cookie %#v = %#v\n", k, v) - http.SetCookie(w, &http.Cookie{ - Name: k, - Value: v, - HttpOnly: true, - }) - } + for k := range params { + http.SetCookie(w, &http.Cookie{ + Name: k, + Value: params.Get(k), + HttpOnly: true, + }) + } + w.Header().Set("Location", "/cookies") + w.WriteHeader(http.StatusFound) +} + +// DeleteCookies deletes cookies specified in query params and redirects to +// Cookies endpoint +func (h *HTTPBin) DeleteCookies(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + for k := range params { + http.SetCookie(w, &http.Cookie{ + Name: k, + Value: params.Get(k), + HttpOnly: true, + MaxAge: -1, + Expires: time.Now().Add(-1 * 24 * 365 * time.Hour), + }) } w.Header().Set("Location", "/cookies") w.WriteHeader(http.StatusFound) diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 883db790fb845b0f2541e8b4183805abc60d89b6..97b87cc1706315c9f0580ce0fa54ff7164cafb68 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" "testing" + "time" ) const maxMemory = 1024 * 1024 @@ -812,13 +813,11 @@ func TestRedirects(t *testing.T) { func TestCookies(t *testing.T) { testCookies := func(t *testing.T, cookies cookiesResponse) { r, _ := http.NewRequest("GET", "/cookies", nil) - for k, vs := range cookies { - for _, v := range vs { - r.AddCookie(&http.Cookie{ - Name: k, - Value: v, - }) - } + for k, v := range cookies { + r.AddCookie(&http.Cookie{ + Name: k, + Value: v, + }) } w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -841,28 +840,24 @@ func TestCookies(t *testing.T) { testCookies(t, cookiesResponse{}) }) - t.Run("ok/single cookies", func(t *testing.T) { - testCookies(t, cookiesResponse{ - "k1": {"v1"}, - "k2": {"v2"}, - }) - }) - - t.Run("ok/duplicate cookies", func(t *testing.T) { + t.Run("ok/cookies", func(t *testing.T) { testCookies(t, cookiesResponse{ - "k1": {"v1"}, - "k2": {"v2a", "v2b"}, + "k1": "v1", + "k2": "v2", }) }) } func TestSetCookies(t *testing.T) { cookies := cookiesResponse{ - "k1": {"v1"}, - "k2": {"v2a", "v2b"}, + "k1": "v1", + "k2": "v2", } - params := url.Values(cookies) + params := &url.Values{} + for k, v := range cookies { + params.Set(k, v) + } r, _ := http.NewRequest("GET", fmt.Sprintf("/cookies/set?%s", params.Encode()), nil) w := httptest.NewRecorder() @@ -872,18 +867,44 @@ func TestSetCookies(t *testing.T) { assertHeader(t, w, "Location", "/cookies") for _, c := range w.Result().Cookies() { - values, ok := cookies[c.Name] + v, ok := cookies[c.Name] if !ok { t.Fatalf("got unexpected cookie %s=%s", c.Name, c.Value) } - found := false - for _, v := range values { - if v == c.Value { - found = true - } + if v != c.Value { + t.Fatalf("got cookie %s=%s, expected value in %#v", c.Name, c.Value, v) } - if !found { - t.Fatalf("got cookie %s=%s, expected value in %#v", c.Name, c.Value, values) + } +} + +func TestDeleteCookies(t *testing.T) { + cookies := cookiesResponse{ + "k1": "v1", + "k2": "v2", + } + + toDelete := "k2" + params := &url.Values{} + params.Set(toDelete, "") + + r, _ := http.NewRequest("GET", fmt.Sprintf("/cookies/delete?%s", params.Encode()), nil) + for k, v := range cookies { + r.AddCookie(&http.Cookie{ + Name: k, + Value: v, + }) + } + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, http.StatusFound) + assertHeader(t, w, "Location", "/cookies") + + for _, c := range w.Result().Cookies() { + if c.Name == toDelete { + if time.Now().Sub(c.Expires) < (24*365-1)*time.Hour { + t.Fatalf("expected cookie %s to be deleted; got %#v", toDelete, c) + } } } } diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index cf06ed40659dcb1a237cac8604cd8eb7ef967296..7576af150d693c3e748f31be69d2c6b3869a3656 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -39,7 +39,7 @@ type bodyResponse struct { JSON interface{} `json:"json"` } -type cookiesResponse map[string][]string +type cookiesResponse map[string]string // Options are used to configure HTTPBin type Options struct { @@ -77,6 +77,7 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/cookies", h.Cookies) mux.HandleFunc("/cookies/set", h.SetCookies) + mux.HandleFunc("/cookies/delete", h.DeleteCookies) // Make sure our ServeMux doesn't "helpfully" redirect these invalid // endpoints by adding a trailing slash. See the ServeMux docs for more