diff --git a/chronograf.go b/chronograf.go index 83a246703b..33ab74f204 100644 --- a/chronograf.go +++ b/chronograf.go @@ -42,6 +42,19 @@ type Logger interface { Writer() *io.PipeWriter } +// Router is an abstracted Router based on the API provided by the +// julienschmidt/httprouter package. +type Router interface { + http.Handler + GET(string, http.HandlerFunc) + PATCH(string, http.HandlerFunc) + POST(string, http.HandlerFunc) + DELETE(string, http.HandlerFunc) + PUT(string, http.HandlerFunc) + + Handler(string, string, http.Handler) +} + // Assets returns a handler to serve the website. type Assets interface { Handler() http.Handler diff --git a/server/mountable_router.go b/server/mountable_router.go new file mode 100644 index 0000000000..387c0016b5 --- /dev/null +++ b/server/mountable_router.go @@ -0,0 +1,58 @@ +package server + +import ( + "net/http" + + "github.com/influxdata/chronograf" +) + +var _ chronograf.Router = &MountableRouter{} + +// MountableRouter is an implementation of a chronograf.Router which supports +// prefixing each route of a Delegated chronograf.Router with a prefix. +type MountableRouter struct { + Prefix string + Delegate chronograf.Router +} + +// DELETE defines a route responding to a DELETE request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) DELETE(path string, handler http.HandlerFunc) { + mr.Delegate.DELETE(mr.Prefix+path, handler) +} + +// GET defines a route responding to a GET request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) GET(path string, handler http.HandlerFunc) { + mr.Delegate.GET(mr.Prefix+path, handler) +} + +// POST defines a route responding to a POST request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) POST(path string, handler http.HandlerFunc) { + mr.Delegate.POST(mr.Prefix+path, handler) +} + +// PUT defines a route responding to a PUT request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) PUT(path string, handler http.HandlerFunc) { + mr.Delegate.PUT(mr.Prefix+path, handler) +} + +// PATCH defines a route responding to a PATCH request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) PATCH(path string, handler http.HandlerFunc) { + mr.Delegate.PATCH(mr.Prefix+path, handler) +} + +// Handler defines a prefixed route responding to a request type specified in +// the method parameter +func (mr *MountableRouter) Handler(method string, path string, handler http.Handler) { + mr.Delegate.Handler(method, mr.Prefix+path, handler) +} + +// ServeHTTP is an implementation of http.Handler which delegates to the +// configured Delegate's implementation of http.Handler +func (mr *MountableRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + mr.Delegate.ServeHTTP(rw, r) +} diff --git a/server/mountable_router_test.go b/server/mountable_router_test.go new file mode 100644 index 0000000000..6c8b2bb392 --- /dev/null +++ b/server/mountable_router_test.go @@ -0,0 +1,240 @@ +package server_test + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/bouk/httprouter" + "github.com/influxdata/chronograf/server" +) + +func Test_MountableRouter_MountsRoutesUnderPrefix(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + expected := "Hello?! McFly?! Anybody in there?!" + mr.GET("/biff", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fmt.Fprintf(rw, expected) + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/chronograf/biff") + if err != nil { + t.Fatal("Unexpected error fetching from mounted router: err:", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Unexpected error decoding response body: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if string(body) != expected { + t.Fatalf("Unexpected response body: Want: \"%s\". Got: \"%s\"", expected, string(body)) + } +} + +func Test_MountableRouter_PrefixesPosts(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + expected := "Great Scott!" + actual := make([]byte, len(expected)) + mr.POST("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if _, err := io.ReadFull(r.Body, actual); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + } else { + rw.WriteHeader(http.StatusOK) + } + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + resp, err := http.Post(ts.URL+"/chronograf/doc", "text/plain", strings.NewReader(expected)) + if err != nil { + t.Fatal("Unexpected error posting to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if string(actual) != expected { + t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual)) + } +} + +func Test_MountableRouter_PrefixesPuts(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + expected := "Great Scott!" + actual := make([]byte, len(expected)) + mr.PUT("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if _, err := io.ReadFull(r.Body, actual); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + } else { + rw.WriteHeader(http.StatusOK) + } + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + req := httptest.NewRequest(http.MethodPut, ts.URL+"/chronograf/doc", strings.NewReader(expected)) + req.Header.Set("Content-Type", "text/plain; charset=utf-8") + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(expected))) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error posting to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if string(actual) != expected { + t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual)) + } +} + +func Test_MountableRouter_PrefixesDeletes(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + mr.DELETE("/proto1985", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusNoContent) + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + req := httptest.NewRequest(http.MethodDelete, ts.URL+"/chronograf/proto1985", nil) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error sending request to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Fatal("Expected 204 but received", resp.StatusCode) + } +} + +func Test_MountableRouter_PrefixesPatches(t *testing.T) { + t.Parallel() + + type Character struct { + Name string + Items []string + } + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + biff := Character{"biff", []string{"sports almanac"}} + mr.PATCH("/1955", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + c := Character{} + err := json.NewDecoder(r.Body).Decode(&c) + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + } else { + biff.Items = c.Items + rw.WriteHeader(http.StatusOK) + } + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + r, w := io.Pipe() + go func() { + _ = json.NewEncoder(w).Encode(Character{"biff", []string{}}) + w.Close() + }() + + req := httptest.NewRequest(http.MethodPatch, ts.URL+"/chronograf/1955", r) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error sending request to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if len(biff.Items) != 0 { + t.Fatal("Failed to alter history, biff still has the sports almanac") + } +} + +func Test_MountableRouter_PrefixesHandler(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + mr.Handler(http.MethodGet, "/recklessAmountOfPower", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte("1.21 Gigawatts!")) + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + req := httptest.NewRequest(http.MethodGet, ts.URL+"/chronograf/recklessAmountOfPower", nil) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error sending request to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } +}