From 6958c3a8bec22b943ae89b602035b7574078b83d Mon Sep 17 00:00:00 2001 From: Aren Patel Date: Thu, 16 Feb 2017 18:47:43 -0800 Subject: [PATCH] Added tests for the REST endpoints exposed in our example. Added a script for running the example app. Split the example into multiple files. --- examples/app.go | 224 ++------------------------------------- examples/fixtures.go | 66 ++++++++++++ examples/handler.go | 113 ++++++++++++++++++++ examples/handler_test.go | 86 +++++++++++++++ examples/models.go | 51 +++++++++ script/example | 4 + 6 files changed, 326 insertions(+), 218 deletions(-) create mode 100644 examples/fixtures.go create mode 100644 examples/handler.go create mode 100644 examples/handler_test.go create mode 100644 examples/models.go create mode 100755 script/example diff --git a/examples/app.go b/examples/app.go index 3326688..0e3b2a3 100644 --- a/examples/app.go +++ b/examples/app.go @@ -8,98 +8,11 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "reflect" - "regexp" - "strconv" "time" "github.com/google/jsonapi" ) -func createBlog(w http.ResponseWriter, r *http.Request) { - jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create") - - blog := new(Blog) - - if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // ...do stuff with your blog... - - w.WriteHeader(http.StatusCreated) - w.Header().Set("Content-Type", jsonapi.MediaType) - - if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func listBlogs(w http.ResponseWriter, r *http.Request) { - jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") - // ...fetch your blogs, filter, offset, limit, etc... - - // but, for now - blogs := testBlogsForList() - - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", jsonapi.MediaType) - if err := jsonapiRuntime.MarshalManyPayload(w, blogs); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func showBlog(w http.ResponseWriter, r *http.Request) { - id := r.FormValue("id") - - // ...fetch your blog... - - intID, err := strconv.Atoi(id) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show") - - // but, for now - blog := testBlogForCreate(intID) - w.WriteHeader(http.StatusOK) - - w.Header().Set("Content-Type", jsonapi.MediaType) - if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func echoBlogs(w http.ResponseWriter, r *http.Request) { - jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.echo") - - // Fetch the blogs from the HTTP request body - data, err := jsonapiRuntime.UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog))) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - // Type assert the []interface{} to []*Blog - blogs := []*Blog{} - for _, b := range data { - blog, ok := b.(*Blog) - if !ok { - http.Error(w, "Unexpected type", http.StatusInternalServerError) - } - blogs = append(blogs, blog) - } - - // Echo the blogs to the response body - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", jsonapi.MediaType) - if err := jsonapiRuntime.MarshalManyPayload(w, blogs); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - func main() { jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) { metricPrefix := r.Value("instrument").(string) @@ -121,91 +34,11 @@ func main() { } } - http.HandleFunc("/blogs", func(w http.ResponseWriter, r *http.Request) { - if !regexp.MustCompile(`application/vnd\.api\+json`).Match([]byte(r.Header.Get("Accept"))) { - http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType) - return - } - - if r.Method == http.MethodPost { - createBlog(w, r) - } else if r.Method == http.MethodPut { - echoBlogs(w, r) - } else if r.FormValue("id") != "" { - showBlog(w, r) - } else { - listBlogs(w, r) - } - }) - + exampleHandler := &ExampleHandler{} + http.HandleFunc("/blogs", exampleHandler.ServeHTTP) exerciseHandler() } -func testBlogForCreate(i int) *Blog { - return &Blog{ - ID: 1 * i, - Title: "Title 1", - CreatedAt: time.Now(), - Posts: []*Post{ - &Post{ - ID: 1 * i, - Title: "Foo", - Body: "Bar", - Comments: []*Comment{ - &Comment{ - ID: 1 * i, - Body: "foo", - }, - &Comment{ - ID: 2 * i, - Body: "bar", - }, - }, - }, - &Post{ - ID: 2 * i, - Title: "Fuubar", - Body: "Bas", - Comments: []*Comment{ - &Comment{ - ID: 1 * i, - Body: "foo", - }, - &Comment{ - ID: 3 * i, - Body: "bas", - }, - }, - }, - }, - CurrentPost: &Post{ - ID: 1 * i, - Title: "Foo", - Body: "Bar", - Comments: []*Comment{ - &Comment{ - ID: 1 * i, - Body: "foo", - }, - &Comment{ - ID: 2 * i, - Body: "bar", - }, - }, - }, - } -} - -func testBlogsForList() []interface{} { - blogs := make([]interface{}, 0, 10) - - for i := 0; i < 10; i++ { - blogs = append(blogs, testBlogForCreate(i)) - } - - return blogs -} - func exerciseHandler() { // list req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) @@ -242,7 +75,7 @@ func exerciseHandler() { fmt.Println("============== end raw jsonapi from show =============") // create - blog := testBlogForCreate(1) + blog := fixtureBlogCreate(1) in := bytes.NewBuffer(nil) jsonapi.MarshalOnePayloadEmbedded(in, blog) @@ -265,9 +98,9 @@ func exerciseHandler() { // echo blogs := []interface{}{ - testBlogForCreate(1), - testBlogForCreate(2), - testBlogForCreate(3), + fixtureBlogCreate(1), + fixtureBlogCreate(2), + fixtureBlogCreate(3), } in = bytes.NewBuffer(nil) jsonapi.MarshalManyPayload(in, blogs) @@ -300,48 +133,3 @@ func exerciseHandler() { fmt.Println(string(out.Bytes())) fmt.Println("================ end marshal materialized Blog struct =================") } - -type Blog struct { - ID int `jsonapi:"primary,blogs"` - Title string `jsonapi:"attr,title"` - Posts []*Post `jsonapi:"relation,posts"` - CurrentPost *Post `jsonapi:"relation,current_post"` - CurrentPostID int `jsonapi:"attr,current_post_id"` - CreatedAt time.Time `jsonapi:"attr,created_at"` - ViewCount int `jsonapi:"attr,view_count"` -} - -type Post struct { - ID int `jsonapi:"primary,posts"` - BlogID int `jsonapi:"attr,blog_id"` - Title string `jsonapi:"attr,title"` - Body string `jsonapi:"attr,body"` - Comments []*Comment `jsonapi:"relation,comments"` -} - -type Comment struct { - ID int `jsonapi:"primary,comments"` - PostID int `jsonapi:"attr,post_id"` - Body string `jsonapi:"attr,body"` -} - -// Blog Links -func (blog Blog) JSONAPILinks() *map[string]interface{} { - return &map[string]interface{}{ - "self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID), - } -} - -func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} { - if relation == "posts" { - return &map[string]interface{}{ - "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), - } - } - if relation == "current_post" { - return &map[string]interface{}{ - "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), - } - } - return nil -} diff --git a/examples/fixtures.go b/examples/fixtures.go new file mode 100644 index 0000000..220c6d7 --- /dev/null +++ b/examples/fixtures.go @@ -0,0 +1,66 @@ +package main + +import "time" + +func fixtureBlogCreate(i int) *Blog { + return &Blog{ + ID: 1 * i, + Title: "Title 1", + CreatedAt: time.Now(), + Posts: []*Post{ + &Post{ + ID: 1 * i, + Title: "Foo", + Body: "Bar", + Comments: []*Comment{ + &Comment{ + ID: 1 * i, + Body: "foo", + }, + &Comment{ + ID: 2 * i, + Body: "bar", + }, + }, + }, + &Post{ + ID: 2 * i, + Title: "Fuubar", + Body: "Bas", + Comments: []*Comment{ + &Comment{ + ID: 1 * i, + Body: "foo", + }, + &Comment{ + ID: 3 * i, + Body: "bas", + }, + }, + }, + }, + CurrentPost: &Post{ + ID: 1 * i, + Title: "Foo", + Body: "Bar", + Comments: []*Comment{ + &Comment{ + ID: 1 * i, + Body: "foo", + }, + &Comment{ + ID: 2 * i, + Body: "bar", + }, + }, + }, + } +} + +func fixtureBlogsList() (blogs []interface{}) { + for i := 0; i < 10; i++ { + blogs = append(blogs, fixtureBlogCreate(i)) + } + + return blogs +} diff --git a/examples/handler.go b/examples/handler.go new file mode 100644 index 0000000..eaf41d0 --- /dev/null +++ b/examples/handler.go @@ -0,0 +1,113 @@ +package main + +import ( + "net/http" + "strconv" + + "github.com/google/jsonapi" +) + +const ( + headerAccept = "Accept" + headerContentType = "Content-Type" +) + +// ExampleHandler is the handler we are using to demonstrate building an HTTP +// server with the jsonapi library. +type ExampleHandler struct{} + +func (h *ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Header.Get(headerAccept) != jsonapi.MediaType { + http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType) + } + + var methodHandler http.HandlerFunc + switch r.Method { + case http.MethodPost: + methodHandler = h.createBlog + case http.MethodPut: + methodHandler = h.echoBlogs + case http.MethodGet: + if r.FormValue("id") != "" { + methodHandler = h.showBlog + } else { + methodHandler = h.listBlogs + } + default: + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + methodHandler(w, r) +} + +func (h *ExampleHandler) createBlog(w http.ResponseWriter, r *http.Request) { + jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create") + + blog := new(Blog) + + if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // ...do stuff with your blog... + + w.WriteHeader(http.StatusCreated) + w.Header().Set(headerContentType, jsonapi.MediaType) + + if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *ExampleHandler) echoBlogs(w http.ResponseWriter, r *http.Request) { + jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") + // ...fetch your blogs, filter, offset, limit, etc... + + // but, for now + blogs := fixtureBlogsList() + + w.WriteHeader(http.StatusOK) + w.Header().Set(headerContentType, jsonapi.MediaType) + if err := jsonapiRuntime.MarshalManyPayload(w, blogs); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *ExampleHandler) showBlog(w http.ResponseWriter, r *http.Request) { + id := r.FormValue("id") + + // ...fetch your blog... + + intID, err := strconv.Atoi(id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show") + + // but, for now + blog := fixtureBlogCreate(intID) + w.WriteHeader(http.StatusOK) + + w.Header().Set(headerContentType, jsonapi.MediaType) + if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *ExampleHandler) listBlogs(w http.ResponseWriter, r *http.Request) { + jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") + // ...fetch your blogs, filter, offset, limit, etc... + + // but, for now + blogs := fixtureBlogsList() + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", jsonapi.MediaType) + if err := jsonapiRuntime.MarshalManyPayload(w, blogs); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/examples/handler_test.go b/examples/handler_test.go new file mode 100644 index 0000000..4edda86 --- /dev/null +++ b/examples/handler_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/jsonapi" +) + +func TestExampleHandler_post(t *testing.T) { + blog := fixtureBlogCreate(1) + requestBody := bytes.NewBuffer(nil) + jsonapi.MarshalOnePayloadEmbedded(requestBody, blog) + + r, err := http.NewRequest(http.MethodPost, "/blogs?id=1", requestBody) + if err != nil { + t.Fatal(err) + } + r.Header.Set(headerAccept, jsonapi.MediaType) + + rr := httptest.NewRecorder() + handler := &ExampleHandler{} + handler.ServeHTTP(rr, r) + + if e, a := http.StatusCreated, rr.Code; e != a { + t.Fatalf("Expected a status of %d, got %d", e, a) + } +} + +func TestExampleHandler_put(t *testing.T) { + blogs := []interface{}{ + fixtureBlogCreate(1), + fixtureBlogCreate(2), + fixtureBlogCreate(3), + } + requestBody := bytes.NewBuffer(nil) + jsonapi.MarshalManyPayload(requestBody, blogs) + + r, err := http.NewRequest(http.MethodPut, "/blogs", requestBody) + if err != nil { + t.Fatal(err) + } + r.Header.Set(headerAccept, jsonapi.MediaType) + + rr := httptest.NewRecorder() + handler := &ExampleHandler{} + handler.ServeHTTP(rr, r) + + if e, a := http.StatusOK, rr.Code; e != a { + t.Fatalf("Expected a status of %d, got %d", e, a) + } +} + +func TestExampleHandler_get_show(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "/blogs?id=1", nil) + if err != nil { + t.Fatal(err) + } + r.Header.Set(headerAccept, jsonapi.MediaType) + + rr := httptest.NewRecorder() + handler := &ExampleHandler{} + handler.ServeHTTP(rr, r) + + if e, a := http.StatusOK, rr.Code; e != a { + t.Fatalf("Expected a status of %d, got %d", e, a) + } +} + +func TestExampleHandler_get_list(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "/blogs", nil) + if err != nil { + t.Fatal(err) + } + r.Header.Set(headerAccept, jsonapi.MediaType) + + rr := httptest.NewRecorder() + handler := &ExampleHandler{} + handler.ServeHTTP(rr, r) + + if e, a := http.StatusOK, rr.Code; e != a { + t.Fatalf("Expected a status of %d, got %d", e, a) + } +} diff --git a/examples/models.go b/examples/models.go new file mode 100644 index 0000000..c28a303 --- /dev/null +++ b/examples/models.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "time" +) + +type Blog struct { + ID int `jsonapi:"primary,blogs"` + Title string `jsonapi:"attr,title"` + Posts []*Post `jsonapi:"relation,posts"` + CurrentPost *Post `jsonapi:"relation,current_post"` + CurrentPostID int `jsonapi:"attr,current_post_id"` + CreatedAt time.Time `jsonapi:"attr,created_at"` + ViewCount int `jsonapi:"attr,view_count"` +} + +type Post struct { + ID int `jsonapi:"primary,posts"` + BlogID int `jsonapi:"attr,blog_id"` + Title string `jsonapi:"attr,title"` + Body string `jsonapi:"attr,body"` + Comments []*Comment `jsonapi:"relation,comments"` +} + +type Comment struct { + ID int `jsonapi:"primary,comments"` + PostID int `jsonapi:"attr,post_id"` + Body string `jsonapi:"attr,body"` +} + +// Blog Links +func (blog Blog) JSONAPILinks() *map[string]interface{} { + return &map[string]interface{}{ + "self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID), + } +} + +func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} { + if relation == "posts" { + return &map[string]interface{}{ + "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), + } + } + if relation == "current_post" { + return &map[string]interface{}{ + "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), + } + } + return nil +} diff --git a/script/example b/script/example new file mode 100755 index 0000000..ff70aa7 --- /dev/null +++ b/script/example @@ -0,0 +1,4 @@ +#!/bin/bash + +set -e +go run examples/app.go examples/handler.go examples/fixtures.go examples/models.go