Last active
October 21, 2015 11:45
-
-
Save trapias/4fa5ac21e418d27bc2b6 to your computer and use it in GitHub Desktop.
api2go_test.go with test demostrating json.RawMessage base64 encoding problem
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package api2go | |
import ( | |
"encoding/json" | |
"errors" | |
"fmt" | |
"net/http" | |
"net/http/httptest" | |
"strconv" | |
"strings" | |
"github.com/manyminds/api2go/jsonapi" | |
. "github.com/onsi/ginkgo" | |
. "github.com/onsi/gomega" | |
"gopkg.in/guregu/null.v2" | |
) | |
type requestURLResolver struct { | |
r http.Request | |
calls int | |
} | |
func (m requestURLResolver) GetBaseURL() string { | |
if uri := m.r.Header.Get("REQUEST_URI"); uri != "" { | |
return uri | |
} | |
return "https://example.com" | |
} | |
func (m *requestURLResolver) SetRequest(r http.Request) { | |
m.r = r | |
} | |
type invalid string | |
func (i invalid) GetID() string { | |
return "invalid" | |
} | |
type Post struct { | |
ID string `jsonapi:"-"` | |
Title string | |
Value null.Float | |
Author *User `jsonapi:"-"` | |
Comments []Comment `jsonapi:"-"` | |
Bananas []Banana `jsonapi:"-"` | |
} | |
func (p Post) GetID() string { | |
return p.ID | |
} | |
func (p *Post) SetID(ID string) error { | |
p.ID = ID | |
return nil | |
} | |
func (p Post) GetReferences() []jsonapi.Reference { | |
return []jsonapi.Reference{ | |
{ | |
Name: "author", | |
Type: "users", | |
}, | |
{ | |
Name: "comments", | |
Type: "comments", | |
}, | |
{ | |
Name: "bananas", | |
Type: "bananas", | |
}, | |
} | |
} | |
func (p Post) GetReferencedIDs() []jsonapi.ReferenceID { | |
result := []jsonapi.ReferenceID{} | |
if p.Author != nil { | |
result = append(result, jsonapi.ReferenceID{ID: p.Author.GetID(), Name: "author", Type: "users"}) | |
} | |
for _, comment := range p.Comments { | |
result = append(result, jsonapi.ReferenceID{ID: comment.GetID(), Name: "comments", Type: "comments"}) | |
} | |
for _, banana := range p.Bananas { | |
result = append(result, jsonapi.ReferenceID{ID: banana.GetID(), Name: "bananas", Type: "bananas"}) | |
} | |
return result | |
} | |
func (p *Post) SetToOneReferenceID(name, ID string) error { | |
if name == "author" { | |
if ID == "" { | |
p.Author = nil | |
} else { | |
p.Author = &User{ID: ID} | |
} | |
return nil | |
} | |
return errors.New("There is no to-one relationship with the name " + name) | |
} | |
func (p *Post) SetToManyReferenceIDs(name string, IDs []string) error { | |
if name == "comments" { | |
comments := []Comment{} | |
for _, ID := range IDs { | |
comments = append(comments, Comment{ID: ID}) | |
} | |
p.Comments = comments | |
} | |
if name == "bananas" { | |
bananas := []Banana{} | |
for _, ID := range IDs { | |
bananas = append(bananas, Banana{ID: ID}) | |
} | |
p.Bananas = bananas | |
} | |
return errors.New("There is no to-many relationship with the name " + name) | |
} | |
func (p *Post) AddToManyIDs(name string, IDs []string) error { | |
if name == "comments" { | |
for _, ID := range IDs { | |
p.Comments = append(p.Comments, Comment{ID: ID}) | |
} | |
} | |
if name == "bananas" { | |
for _, ID := range IDs { | |
p.Bananas = append(p.Bananas, Banana{ID: ID}) | |
} | |
} | |
return errors.New("There is no to-manyrelationship with the name " + name) | |
} | |
func (p *Post) DeleteToManyIDs(name string, IDs []string) error { | |
if name == "comments" { | |
for _, ID := range IDs { | |
// find and delete the comment with ID | |
for pos, comment := range p.Comments { | |
if comment.GetID() == ID { | |
p.Comments = append(p.Comments[:pos], p.Comments[pos+1:]...) | |
} | |
} | |
} | |
} | |
if name == "bananas" { | |
for _, ID := range IDs { | |
// find and delete the comment with ID | |
for pos, banana := range p.Bananas { | |
if banana.GetID() == ID { | |
p.Bananas = append(p.Bananas[:pos], p.Bananas[pos+1:]...) | |
} | |
} | |
} | |
} | |
return errors.New("There is no to-manyrelationship with the name " + name) | |
} | |
func (p Post) GetReferencedStructs() []jsonapi.MarshalIdentifier { | |
result := []jsonapi.MarshalIdentifier{} | |
if p.Author != nil { | |
result = append(result, *p.Author) | |
} | |
for key := range p.Comments { | |
result = append(result, p.Comments[key]) | |
} | |
for key := range p.Bananas { | |
result = append(result, p.Bananas[key]) | |
} | |
return result | |
} | |
type Comment struct { | |
ID string `jsonapi:"-"` | |
Value string | |
} | |
func (c Comment) GetID() string { | |
return c.ID | |
} | |
type Banana struct { | |
ID string `jnson:"-"` | |
Name string | |
} | |
func (b Banana) GetID() string { | |
return b.ID | |
} | |
type User struct { | |
ID string `jsonapi:"-"` | |
Name string | |
Profile json.RawMessage `jsonapi:"name=profile"` | |
} | |
func (u User) GetID() string { | |
return u.ID | |
} | |
type fixtureSource struct { | |
posts map[string]*Post | |
pointers bool | |
} | |
func (s *fixtureSource) FindAll(req Request) (Responder, error) { | |
var err error | |
if limit, ok := req.QueryParams["limit"]; ok { | |
if l, err := strconv.ParseInt(limit[0], 10, 64); err == nil { | |
if s.pointers { | |
postsSlice := make([]*Post, l) | |
length := len(s.posts) | |
for i := 0; i < length; i++ { | |
postsSlice[i] = s.posts[strconv.Itoa(i+1)] | |
if i+1 >= int(l) { | |
break | |
} | |
} | |
return &Response{Res: postsSlice}, nil | |
} | |
postsSlice := make([]Post, l) | |
length := len(s.posts) | |
for i := 0; i < length; i++ { | |
postsSlice[i] = *s.posts[strconv.Itoa(i+1)] | |
if i+1 >= int(l) { | |
break | |
} | |
} | |
return &Response{Res: postsSlice}, nil | |
} | |
fmt.Println("Error casting to int", err) | |
return &Response{}, err | |
} | |
if s.pointers { | |
postsSlice := make([]Post, len(s.posts)) | |
length := len(s.posts) | |
for i := 0; i < length; i++ { | |
postsSlice[i] = *s.posts[strconv.Itoa(i+1)] | |
} | |
return &Response{Res: postsSlice}, nil | |
} | |
postsSlice := make([]*Post, len(s.posts)) | |
length := len(s.posts) | |
for i := 0; i < length; i++ { | |
postsSlice[i] = s.posts[strconv.Itoa(i+1)] | |
} | |
return &Response{Res: postsSlice}, nil | |
} | |
// this does not read the query parameters, which you would do to limit the result in real world usage | |
func (s *fixtureSource) PaginatedFindAll(req Request) (uint, Responder, error) { | |
if s.pointers { | |
postsSlice := []*Post{} | |
for _, post := range s.posts { | |
postsSlice = append(postsSlice, post) | |
} | |
return uint(len(s.posts)), &Response{Res: postsSlice}, nil | |
} | |
postsSlice := []Post{} | |
for _, post := range s.posts { | |
postsSlice = append(postsSlice, *post) | |
} | |
return uint(len(s.posts)), &Response{Res: postsSlice}, nil | |
} | |
func (s *fixtureSource) FindOne(id string, req Request) (Responder, error) { | |
if p, ok := s.posts[id]; ok { | |
if s.pointers { | |
return &Response{Res: p}, nil | |
} | |
return &Response{Res: *p}, nil | |
} | |
return nil, NewHTTPError(nil, "post not found", http.StatusNotFound) | |
} | |
func (s *fixtureSource) Create(obj interface{}, req Request) (Responder, error) { | |
var p *Post | |
if s.pointers { | |
p = obj.(*Post) | |
} else { | |
o := obj.(Post) | |
p = &o | |
} | |
if p.Title == "" { | |
err := NewHTTPError(errors.New("Bad request."), "Bad Request", http.StatusBadRequest) | |
err.Errors = append(err.Errors, Error{ID: "SomeErrorID", Source: &ErrorSource{Pointer: "Title"}}) | |
return &Response{}, err | |
} | |
maxID := 0 | |
for k := range s.posts { | |
id, _ := strconv.Atoi(k) | |
if id > maxID { | |
maxID = id | |
} | |
} | |
newID := strconv.Itoa(maxID + 1) | |
p.ID = newID | |
s.posts[newID] = p | |
return &Response{Res: p, Code: http.StatusCreated}, nil | |
} | |
func (s *fixtureSource) Delete(id string, req Request) (Responder, error) { | |
delete(s.posts, id) | |
return &Response{Code: http.StatusNoContent}, nil | |
} | |
func (s *fixtureSource) Update(obj interface{}, req Request) (Responder, error) { | |
var p *Post | |
if s.pointers { | |
p = obj.(*Post) | |
} else { | |
o := obj.(Post) | |
p = &o | |
} | |
if oldP, ok := s.posts[p.ID]; ok { | |
oldP.Title = p.Title | |
oldP.Author = p.Author | |
oldP.Comments = p.Comments | |
return &Response{Code: http.StatusNoContent}, nil | |
} | |
return &Response{}, NewHTTPError(nil, "post not found", http.StatusNotFound) | |
} | |
type userSource struct { | |
pointers bool | |
} | |
func (s *userSource) FindAll(req Request) (Responder, error) { | |
postsIDs, ok := req.QueryParams["postsID"] | |
if ok { | |
if postsIDs[0] == "1" { | |
u := User{ID: "1", Name: "Dieter", Profile: json.RawMessage("{image: null}")} | |
if s.pointers { | |
return &Response{Res: &u}, nil | |
} | |
return &Response{Res: u}, nil | |
} | |
} | |
if s.pointers { | |
return &Response{}, errors.New("Did not receive query parameter") | |
} | |
return &Response{}, errors.New("Did not receive query parameter") | |
} | |
func (s *userSource) FindOne(id string, req Request) (Responder, error) { | |
return &Response{}, nil | |
} | |
func (s *userSource) Create(obj interface{}, req Request) (Responder, error) { | |
return &Response{Res: obj, Code: http.StatusCreated}, nil | |
} | |
func (s *userSource) Delete(id string, req Request) (Responder, error) { | |
return &Response{Code: http.StatusNoContent}, nil | |
} | |
func (s *userSource) Update(obj interface{}, req Request) (Responder, error) { | |
return &Response{}, NewHTTPError(nil, "user not found", http.StatusNotFound) | |
} | |
type commentSource struct { | |
pointers bool | |
} | |
func (s *commentSource) FindAll(req Request) (Responder, error) { | |
postsIDs, ok := req.QueryParams["postsID"] | |
if ok { | |
if postsIDs[0] == "1" { | |
c := Comment{ | |
ID: "1", | |
Value: "This is a stupid post!", | |
} | |
if s.pointers { | |
return &Response{Res: []*Comment{&c}}, nil | |
} | |
return &Response{Res: []Comment{c}}, nil | |
} | |
} | |
if s.pointers { | |
return &Response{Res: []*Comment{}}, errors.New("Did not receive query parameter") | |
} | |
return &Response{Res: []Comment{}}, errors.New("Did not receive query parameter") | |
} | |
func (s *commentSource) FindOne(id string, req Request) (Responder, error) { | |
return &Response{}, nil | |
} | |
func (s *commentSource) Create(obj interface{}, req Request) (Responder, error) { | |
return &Response{Code: http.StatusCreated, Res: obj}, nil | |
} | |
func (s *commentSource) Delete(id string, req Request) (Responder, error) { | |
return &Response{Code: http.StatusNoContent}, nil | |
} | |
func (s *commentSource) Update(obj interface{}, req Request) (Responder, error) { | |
return &Response{}, NewHTTPError(nil, "comment not found", http.StatusNotFound) | |
} | |
type prettyJSONContentMarshaler struct { | |
} | |
func (m prettyJSONContentMarshaler) Marshal(i interface{}) ([]byte, error) { | |
return json.MarshalIndent(i, "", " ") | |
} | |
func (m prettyJSONContentMarshaler) Unmarshal(data []byte, i interface{}) error { | |
return json.Unmarshal(data, i) | |
} | |
func (m prettyJSONContentMarshaler) MarshalError(err error) string { | |
jsonmarshaler := JSONContentMarshaler{} | |
return jsonmarshaler.MarshalError(err) | |
} | |
var _ = Describe("RestHandler", func() { | |
var usePointerResources bool | |
requestHandlingTests := func() { | |
var ( | |
source *fixtureSource | |
post1Json map[string]interface{} | |
post1LinkedJSON []map[string]interface{} | |
post2Json map[string]interface{} | |
post3Json map[string]interface{} | |
api *API | |
rec *httptest.ResponseRecorder | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": { | |
ID: "1", | |
Title: "Hello, World!", | |
Author: &User{ | |
ID: "1", | |
Name: "Dieter", | |
Profile: json.RawMessage("{image: null}"), | |
}, | |
Comments: []Comment{{ | |
ID: "1", | |
Value: "This is a stupid post!", | |
}}, | |
}, | |
"2": {ID: "2", Title: "I am NR. 2"}, | |
"3": {ID: "3", Title: "I am NR. 3"}, | |
}, usePointerResources} | |
post1Json = map[string]interface{}{ | |
"id": "1", | |
"type": "posts", | |
"attributes": map[string]interface{}{ | |
"title": "Hello, World!", | |
"value": nil, | |
}, | |
"relationships": map[string]interface{}{ | |
"author": map[string]interface{}{ | |
"data": map[string]interface{}{ | |
"id": "1", | |
"type": "users", | |
}, | |
"links": map[string]string{ | |
"self": "/v1/posts/1/relationships/author", | |
"related": "/v1/posts/1/author", | |
}, | |
}, | |
"comments": map[string]interface{}{ | |
"data": []map[string]interface{}{ | |
{ | |
"id": "1", | |
"type": "comments", | |
}, | |
}, | |
"links": map[string]string{ | |
"self": "/v1/posts/1/relationships/comments", | |
"related": "/v1/posts/1/comments", | |
}, | |
}, | |
"bananas": map[string]interface{}{ | |
"data": []map[string]interface{}{}, | |
"links": map[string]string{ | |
"self": "/v1/posts/1/relationships/bananas", | |
"related": "/v1/posts/1/bananas", | |
}, | |
}, | |
}, | |
} | |
post1LinkedJSON = []map[string]interface{}{ | |
{ | |
"id": "1", | |
"type": "users", | |
"attributes": map[string]interface{}{ | |
"name": "Dieter", | |
"profile": "e2ltYWdlOiBudWxsfQ==", | |
}, | |
}, | |
{ | |
"id": "1", | |
"type": "comments", | |
"attributes": map[string]interface{}{ | |
"value": "This is a stupid post!", | |
}, | |
}, | |
} | |
post2Json = map[string]interface{}{ | |
"id": "2", | |
"type": "posts", | |
"attributes": map[string]interface{}{ | |
"title": "I am NR. 2", | |
"value": nil, | |
}, | |
"relationships": map[string]interface{}{ | |
"author": map[string]interface{}{ | |
"data": nil, | |
"links": map[string]string{ | |
"self": "/v1/posts/2/relationships/author", | |
"related": "/v1/posts/2/author", | |
}, | |
}, | |
"comments": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]string{ | |
"self": "/v1/posts/2/relationships/comments", | |
"related": "/v1/posts/2/comments", | |
}, | |
}, | |
"bananas": map[string]interface{}{ | |
"data": []map[string]interface{}{}, | |
"links": map[string]string{ | |
"self": "/v1/posts/2/relationships/bananas", | |
"related": "/v1/posts/2/bananas", | |
}, | |
}, | |
}, | |
} | |
post3Json = map[string]interface{}{ | |
"id": "3", | |
"type": "posts", | |
"attributes": map[string]interface{}{ | |
"title": "I am NR. 3", | |
"value": nil, | |
}, | |
"relationships": map[string]interface{}{ | |
"author": map[string]interface{}{ | |
"data": nil, | |
"links": map[string]string{ | |
"self": "/v1/posts/3/relationships/author", | |
"related": "/v1/posts/3/author", | |
}, | |
}, | |
"comments": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]string{ | |
"self": "/v1/posts/3/relationships/comments", | |
"related": "/v1/posts/3/comments", | |
}, | |
}, | |
"bananas": map[string]interface{}{ | |
"data": []map[string]interface{}{}, | |
"links": map[string]string{ | |
"self": "/v1/posts/3/relationships/bananas", | |
"related": "/v1/posts/3/bananas", | |
}, | |
}, | |
}, | |
} | |
api = NewAPI("v1") | |
if usePointerResources { | |
api.AddResource(&Post{}, source) | |
api.AddResource(&User{}, &userSource{true}) | |
api.AddResource(&Comment{}, &commentSource{true}) | |
} else { | |
api.AddResource(Post{}, source) | |
api.AddResource(User{}, &userSource{false}) | |
api.AddResource(Comment{}, &commentSource{false}) | |
} | |
rec = httptest.NewRecorder() | |
}) | |
It("GETs collections", func() { | |
req, err := http.NewRequest("GET", "/v1/posts", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
expected, err := json.Marshal(map[string]interface{}{ | |
"data": []map[string]interface{}{post1Json, post2Json, post3Json}, | |
"included": post1LinkedJSON, | |
}) | |
Expect(err).ToNot(HaveOccurred()) | |
Expect(rec.Body.Bytes()).To(MatchJSON(expected)) | |
}) | |
It("GETs single objects", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/1", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
expected, err := json.Marshal(map[string]interface{}{ | |
"data": post1Json, | |
"included": post1LinkedJSON, | |
}) | |
Expect(err).ToNot(HaveOccurred()) | |
Expect(rec.Body.Bytes()).To(MatchJSON(expected)) | |
}) | |
It("GETs related struct from resource url", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/1/author", nil) | |
Expect(err).ToNot(HaveOccurred()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
// "profile": "{image: null}" | |
// "profile": "e2ltYWdlOiBudWxsfQ==" to pass tests, but wrong! | |
Expect(rec.Body.Bytes()).To(MatchJSON(` | |
{"data": { | |
"id": "1", | |
"type": "users", | |
"attributes": { | |
"name": "Dieter", | |
"profile": "{image: null}" | |
} | |
}}`)) | |
}) | |
It("GETs related structs from resource url", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/1/comments", nil) | |
Expect(err).ToNot(HaveOccurred()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.Body.Bytes()).To(MatchJSON(` | |
{"data": [{ | |
"id": "1", | |
"type": "comments", | |
"attributes": { | |
"value": "This is a stupid post!" | |
} | |
}]}`)) | |
}) | |
It("GETs relationship data from relationship url for to-many", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/1/relationships/comments", nil) | |
Expect(err).ToNot(HaveOccurred()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.Body.Bytes()).To(MatchJSON(`{"data": [{"id": "1", "type": "comments"}], "links": {"self": "/v1/posts/1/relationships/comments", "related": "/v1/posts/1/comments"}}`)) | |
}) | |
It("GETs relationship data from relationship url for to-one", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/1/relationships/author", nil) | |
Expect(err).ToNot(HaveOccurred()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.Body.Bytes()).To(MatchJSON(`{"data": {"id": "1", "type": "users"}, "links": {"self": "/v1/posts/1/relationships/author", "related": "/v1/posts/1/author"}}`)) | |
}) | |
It("Gets 404 if a related struct was not found", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/1/unicorns", nil) | |
Expect(err).ToNot(HaveOccurred()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusNotFound)) | |
Expect(rec.Body.Bytes()).ToNot(BeEmpty()) | |
}) | |
It("404s", func() { | |
req, err := http.NewRequest("GET", "/v1/posts/23", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusNotFound)) | |
errorJSON := []byte(`{"errors":[{"status":"404","title":"post not found"}]}`) | |
Expect(rec.Body.Bytes()).To(MatchJSON(errorJSON)) | |
}) | |
It("POSTSs new objects", func() { | |
reqBody := strings.NewReader(`{"data": [{"attributes":{"title": "New Post" }, "type": "posts"}]}`) | |
req, err := http.NewRequest("POST", "/v1/posts", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusCreated)) | |
Expect(rec.Header().Get("Location")).To(Equal("/v1/posts/4")) | |
var result map[string]interface{} | |
Expect(json.Unmarshal(rec.Body.Bytes(), &result)).To(BeNil()) | |
Expect(result).To(Equal(map[string]interface{}{ | |
"data": map[string]interface{}{ | |
"id": "4", | |
"type": "posts", | |
"attributes": map[string]interface{}{ | |
"title": "New Post", | |
"value": nil, | |
}, | |
"relationships": map[string]interface{}{ | |
"author": map[string]interface{}{ | |
"data": nil, | |
"links": map[string]interface{}{ | |
"self": "/v1/posts/4/relationships/author", | |
"related": "/v1/posts/4/author", | |
}, | |
}, | |
"comments": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]interface{}{ | |
"self": "/v1/posts/4/relationships/comments", | |
"related": "/v1/posts/4/comments", | |
}, | |
}, | |
"bananas": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]interface{}{ | |
"self": "/v1/posts/4/relationships/bananas", | |
"related": "/v1/posts/4/bananas", | |
}, | |
}, | |
}, | |
}, | |
})) | |
}) | |
It("POSTSs new objects with trailing slash automatic redirect enabled", func() { | |
reqBody := strings.NewReader(`{"data": [{"title": "New Post", "type": "posts"}]}`) | |
req, err := http.NewRequest("POST", "/v1/posts/", reqBody) | |
Expect(err).To(BeNil()) | |
api.SetRedirectTrailingSlash(true) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusTemporaryRedirect)) | |
}) | |
It("POSTSs with client id", func() { | |
reqBody := strings.NewReader(`{"data": [{"attributes": {"title": "New Post"}, "id": "100", "type": "posts"}]}`) | |
req, err := http.NewRequest("POST", "/v1/posts", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusCreated)) | |
}) | |
It("POSTSs new objects with trailing slash automatic redirect disabled", func() { | |
reqBody := strings.NewReader(`{"data": [{"title": "New Post", "type": "posts"}]}`) | |
req, err := http.NewRequest("POST", "/v1/posts/", reqBody) | |
Expect(err).To(BeNil()) | |
api.SetRedirectTrailingSlash(false) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusNotFound)) | |
}) | |
It("POSTSs multiple objects", func() { | |
reqBody := strings.NewReader(`{"posts": [{"title": "New Post"}, {"title" : "Second New Post"}]}`) | |
req, err := http.NewRequest("POST", "/v1/posts", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusInternalServerError)) | |
Expect(rec.Header().Get("Location")).To(Equal("")) | |
Expect(rec.Body.Bytes()).ToNot(HaveLen(0)) | |
}) | |
It("OPTIONS on collection route", func() { | |
req, err := http.NewRequest("OPTIONS", "/v1/posts", nil) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(err).To(BeNil()) | |
Expect(rec.Code).To(Equal(http.StatusNoContent)) | |
Expect(rec.Header().Get("Allow")).To(Equal("GET,POST,PATCH,OPTIONS")) | |
}) | |
It("OPTIONS on element route", func() { | |
req, err := http.NewRequest("OPTIONS", "/v1/posts/1", nil) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(err).To(BeNil()) | |
Expect(rec.Code).To(Equal(http.StatusNoContent)) | |
Expect(rec.Header().Get("Allow")).To(Equal("GET,PATCH,DELETE,OPTIONS")) | |
}) | |
It("DELETEs", func() { | |
req, err := http.NewRequest("DELETE", "/v1/posts/1", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusNoContent)) | |
Expect(len(source.posts)).To(Equal(2)) | |
}) | |
It("patch must contain type and id but does not have type", func() { | |
reqBody := strings.NewReader(`{"data": {"title": "New Title", "id": "id"}}`) | |
req, err := http.NewRequest("PATCH", "/v1/posts/1", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusForbidden)) | |
Expect(string(rec.Body.Bytes())).To(MatchJSON(`{"errors":[{"status":"403","title":"missing mandatory type key."}]}`)) | |
}) | |
It("patch must contain type and id but does not have id", func() { | |
reqBody := strings.NewReader(`{"data": {"title": "New Title", "type": "posts"}}`) | |
req, err := http.NewRequest("PATCH", "/v1/posts/1", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusForbidden)) | |
Expect(string(rec.Body.Bytes())).To(MatchJSON(`{"errors":[{"status":"403","title":"missing mandatory id key."}]}`)) | |
}) | |
Context("Updating", func() { | |
doRequest := func(payload, url, method string) { | |
reqBody := strings.NewReader(payload) | |
req, err := http.NewRequest(method, url, reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Body.String()).To(Equal("")) | |
Expect(rec.Code).To(Equal(http.StatusNoContent)) | |
} | |
It("UPDATEs", func() { | |
target := source.posts["1"] | |
target.Value = null.FloatFrom(2) | |
doRequest(`{"data": {"id": "1", "attributes": {"title": "New Title"}, "type": "posts"}}`, "/v1/posts/1", "PATCH") | |
Expect(source.posts["1"].Title).To(Equal("New Title")) | |
Expect(target.Title).To(Equal("New Title")) | |
Expect(target.Value).To(Equal(null.FloatFrom(2))) | |
}) | |
It("Patch updates to-one relationships", func() { | |
target := source.posts["1"] | |
doRequest(`{ | |
"data": { | |
"type": "posts", | |
"id": "1", | |
"relationships": { | |
"author": { | |
"data": { | |
"type": "users", | |
"id": "2" | |
} | |
} | |
} | |
} | |
} | |
`, "/v1/posts/1", "PATCH") | |
Expect(target.Author.GetID()).To(Equal("2")) | |
}) | |
It("Patch can delete to-one relationships", func() { | |
target := source.posts["1"] | |
doRequest(`{ | |
"data": { | |
"type": "posts", | |
"id": "1", | |
"relationships": { | |
"author": { | |
"data": null | |
} | |
} | |
} | |
} | |
`, "/v1/posts/1", "PATCH") | |
Expect(target.Author).To(BeNil()) | |
}) | |
It("Patch updates to-many relationships", func() { | |
target := source.posts["1"] | |
doRequest(`{ | |
"data": { | |
"type": "posts", | |
"id": "1", | |
"relationships": { | |
"comments": { | |
"data": [ | |
{ | |
"type": "comments", | |
"id": "2" | |
} | |
] | |
} | |
} | |
} | |
} | |
`, "/v1/posts/1", "PATCH") | |
Expect(target.Comments[0].GetID()).To(Equal("2")) | |
}) | |
It("Patch can delete to-many relationships", func() { | |
target := source.posts["1"] | |
doRequest(`{ | |
"data": { | |
"type": "posts", | |
"id": "1", | |
"relationships": { | |
"comments": { | |
"data": [] | |
} | |
} | |
} | |
} | |
`, "/v1/posts/1", "PATCH") | |
Expect(target.Comments).To(HaveLen(0)) | |
}) | |
It("Relationship PATCH route updates to-one", func() { | |
doRequest(`{ | |
"data": { | |
"type": "users", | |
"id": "2" | |
} | |
}`, "/v1/posts/1/relationships/author", "PATCH") | |
target := source.posts["1"] | |
Expect(target.Author.GetID()).To(Equal("2")) | |
}) | |
It("Relationship PATCH route updates to-many", func() { | |
doRequest(`{ | |
"data": [{ | |
"type": "comments", | |
"id": "2" | |
}] | |
}`, "/v1/posts/1/relationships/comments", "PATCH") | |
target := source.posts["1"] | |
Expect(target.Comments).To(HaveLen(1)) | |
Expect(target.Comments[0].GetID()).To(Equal("2")) | |
}) | |
It("Relationship POST route adds to-many elements", func() { | |
doRequest(`{ | |
"data": [{ | |
"type": "comments", | |
"id": "2" | |
}] | |
}`, "/v1/posts/1/relationships/comments", "POST") | |
target := source.posts["1"] | |
Expect(target.Comments).To(HaveLen(2)) | |
}) | |
It("Relationship DELETE route deletes to-many elements", func() { | |
doRequest(`{ | |
"data": [{ | |
"type": "comments", | |
"id": "1" | |
}] | |
}`, "/v1/posts/1/relationships/comments", "DELETE") | |
target := source.posts["1"] | |
Expect(target.Comments).To(HaveLen(0)) | |
}) | |
}) | |
} | |
usePointerResources = false | |
Context("when handling requests for non-pointer resources", requestHandlingTests) | |
usePointerResources = true | |
Context("when handling requests for pointer resources", requestHandlingTests) | |
Context("marshal errors correctly", func() { | |
var ( | |
source *fixtureSource | |
post1Json map[string]interface{} | |
api *API | |
rec *httptest.ResponseRecorder | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
}, false} | |
post1Json = map[string]interface{}{ | |
"id": "1", | |
"title": "Hello, World!", | |
"value": nil, | |
} | |
api = NewAPI("") | |
api.AddResource(Post{}, source) | |
rec = httptest.NewRecorder() | |
}) | |
It("POSTSs new objects", func() { | |
reqBody := strings.NewReader(`{"data": [{"title": "", "type": "posts"}]}`) | |
req, err := http.NewRequest("POST", "/posts", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusBadRequest)) | |
expected := `{"errors":[{"id":"SomeErrorID","source":{"pointer":"Title"}}]}` | |
actual := strings.TrimSpace(string(rec.Body.Bytes())) | |
Expect(actual).To(Equal(expected)) | |
}) | |
}) | |
Context("use content marshalers correctly", func() { | |
var ( | |
source *fixtureSource | |
api *API | |
rec *httptest.ResponseRecorder | |
jsonResponse string | |
prettyResponse string | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
}, false} | |
jsonResponse = `{"data":{"attributes":{"title":"Hello, World!","value":null},"id":"1","relationships":{"author":{"data":null,"links":{"related":"/posts/1/author","self":"/posts/1/relationships/author"}},"bananas":{"data":[],"links":{"related":"/posts/1/bananas","self":"/posts/1/relationships/bananas"}},"comments":{"data":[],"links":{"related":"/posts/1/comments","self":"/posts/1/relationships/comments"}}},"type":"posts"}}` | |
prettyResponse = `{ | |
"data": { | |
"attributes": { | |
"title": "Hello, World!", | |
"value": null | |
}, | |
"id": "1", | |
"relationships": { | |
"author": { | |
"data": null, | |
"links": { | |
"related": "/posts/1/author", | |
"self": "/posts/1/relationships/author" | |
} | |
}, | |
"bananas": { | |
"data": [], | |
"links": { | |
"related": "/posts/1/bananas", | |
"self": "/posts/1/relationships/bananas" | |
} | |
}, | |
"comments": { | |
"data": [], | |
"links": { | |
"related": "/posts/1/comments", | |
"self": "/posts/1/relationships/comments" | |
} | |
} | |
}, | |
"type": "posts" | |
} | |
}` | |
marshalers := map[string]ContentMarshaler{ | |
`application/vnd.api+json`: JSONContentMarshaler{}, | |
`application/vnd.api+prettyjson`: prettyJSONContentMarshaler{}, | |
} | |
api = NewAPIWithMarshalers("", "", marshalers) | |
api.AddResource(Post{}, source) | |
rec = httptest.NewRecorder() | |
}) | |
It("Selects the default content marshaler when no Content-Type or Accept request header is present", func() { | |
req, err := http.NewRequest("GET", "/posts/1", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.HeaderMap["Content-Type"][0]).To(Equal("application/vnd.api+json")) | |
actual := strings.TrimSpace(string(rec.Body.Bytes())) | |
Expect(actual).To(Equal(jsonResponse)) | |
}) | |
It("Selects the default content marshaler when Content-Type doesn't specify a known content type", func() { | |
req, err := http.NewRequest("GET", "/posts/1", nil) | |
Expect(err).To(BeNil()) | |
req.Header.Set("Content-Type", "application/json") | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.HeaderMap["Content-Type"][0]).To(Equal("application/vnd.api+json")) | |
actual := strings.TrimSpace(string(rec.Body.Bytes())) | |
Expect(actual).To(Equal(jsonResponse)) | |
}) | |
It("Selects the default content marshaler when Accept doesn't specify a known content type", func() { | |
req, err := http.NewRequest("GET", "/posts/1", nil) | |
Expect(err).To(BeNil()) | |
req.Header.Set("Accept", "text/html,application/xml;q=0.9") | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.HeaderMap["Content-Type"][0]).To(Equal("application/vnd.api+json")) | |
actual := strings.TrimSpace(string(rec.Body.Bytes())) | |
Expect(actual).To(Equal(jsonResponse)) | |
}) | |
It("Selects the correct content marshaler when Content-Type specifies a known content type", func() { | |
req, err := http.NewRequest("GET", "/posts/1", nil) | |
Expect(err).To(BeNil()) | |
req.Header.Set("Content-Type", `application/vnd.api+prettyjson`) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.HeaderMap["Content-Type"][0]).To(Equal(`application/vnd.api+prettyjson`)) | |
actual := strings.TrimSpace(string(rec.Body.Bytes())) | |
Expect(actual).To(Equal(prettyResponse)) | |
}) | |
It("Selects the correct content marshaler when Accept specifies a known content type", func() { | |
req, err := http.NewRequest("GET", "/posts/1", nil) | |
Expect(err).To(BeNil()) | |
req.Header.Set("Accept", `text/html,application/xml;q=0.9,application/vnd.api+prettyjson;q=0.5`) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(rec.HeaderMap["Content-Type"][0]).To(Equal(`application/vnd.api+prettyjson`)) | |
actual := strings.TrimSpace(string(rec.Body.Bytes())) | |
Expect(actual).To(Equal(prettyResponse)) | |
}) | |
}) | |
Context("Extracting query parameters with complete BaseURL API", func() { | |
var ( | |
source *fixtureSource | |
post1JSON map[string]interface{} | |
post2JSON map[string]interface{} | |
api *API | |
rec *httptest.ResponseRecorder | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
"2": {ID: "2", Title: "Hello, from second Post!"}, | |
}, false} | |
post1JSON = map[string]interface{}{ | |
"id": "1", | |
"type": "posts", | |
"attributes": map[string]interface{}{ | |
"title": "Hello, World!", | |
"value": nil, | |
}, | |
"relationships": map[string]interface{}{ | |
"author": map[string]interface{}{ | |
"data": nil, | |
"links": map[string]interface{}{ | |
"self": "http://localhost:1337/v0/posts/1/relationships/author", | |
"related": "http://localhost:1337/v0/posts/1/author", | |
}, | |
}, | |
"bananas": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]interface{}{ | |
"self": "http://localhost:1337/v0/posts/1/relationships/bananas", | |
"related": "http://localhost:1337/v0/posts/1/bananas", | |
}, | |
}, | |
"comments": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]interface{}{ | |
"self": "http://localhost:1337/v0/posts/1/relationships/comments", | |
"related": "http://localhost:1337/v0/posts/1/comments", | |
}, | |
}, | |
}, | |
} | |
post2JSON = map[string]interface{}{ | |
"id": "2", | |
"type": "posts", | |
"attributes": map[string]interface{}{ | |
"title": "Hello, from second Post!", | |
"value": nil, | |
}, | |
"relationships": map[string]interface{}{ | |
"author": map[string]interface{}{ | |
"data": nil, | |
"links": map[string]interface{}{ | |
"self": "http://localhost:1337/v0/posts/2/relationships/author", | |
"related": "http://localhost:1337/v0/posts/2/author", | |
}, | |
}, | |
"bananas": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]interface{}{ | |
"self": "http://localhost:1337/v0/posts/2/relationships/bananas", | |
"related": "http://localhost:1337/v0/posts/2/bananas", | |
}, | |
}, | |
"comments": map[string]interface{}{ | |
"data": []interface{}{}, | |
"links": map[string]interface{}{ | |
"self": "http://localhost:1337/v0/posts/2/relationships/comments", | |
"related": "http://localhost:1337/v0/posts/2/comments", | |
}, | |
}, | |
}, | |
} | |
api = NewAPIWithBaseURL("v0", "http://localhost:1337") | |
api.AddResource(Post{}, source) | |
rec = httptest.NewRecorder() | |
}) | |
It("FindAll returns 2 posts if no limit was set", func() { | |
req, err := http.NewRequest("GET", "/v0/posts", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
var result map[string]interface{} | |
Expect(json.Unmarshal(rec.Body.Bytes(), &result)).To(BeNil()) | |
Expect(result).To(Equal(map[string]interface{}{ | |
"data": []interface{}{post1JSON, post2JSON}, | |
})) | |
}) | |
It("FindAll returns 1 post with limit 1", func() { | |
req, err := http.NewRequest("GET", "/v0/posts?limit=1", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
var result map[string]interface{} | |
Expect(json.Unmarshal(rec.Body.Bytes(), &result)).To(BeNil()) | |
Expect(result).To(Equal(map[string]interface{}{ | |
"data": []interface{}{post1JSON}, | |
})) | |
}) | |
It("Extracts multiple parameters correctly", func() { | |
req, err := http.NewRequest("GET", "/v0/posts?sort=title,date", nil) | |
Expect(err).To(BeNil()) | |
c := &APIContext{} | |
api2goReq := buildRequest(c, req) | |
Expect(api2goReq.QueryParams).To(Equal(map[string][]string{"sort": {"title", "date"}})) | |
}) | |
}) | |
Context("When using pagination", func() { | |
var ( | |
api *API | |
rec *httptest.ResponseRecorder | |
source *fixtureSource | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
"2": {ID: "2", Title: "Hello, World!"}, | |
"3": {ID: "3", Title: "Hello, World!"}, | |
"4": {ID: "4", Title: "Hello, World!"}, | |
"5": {ID: "5", Title: "Hello, World!"}, | |
"6": {ID: "6", Title: "Hello, World!"}, | |
"7": {ID: "7", Title: "Hello, World!"}, | |
}, false} | |
api = NewAPI("v1") | |
api.AddResource(Post{}, source) | |
rec = httptest.NewRecorder() | |
}) | |
// helper function that does a request and returns relevant pagination urls out of the response body | |
doRequest := func(URL string) map[string]string { | |
req, err := http.NewRequest("GET", URL, nil) | |
Expect(err).ToNot(HaveOccurred()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
var response map[string]interface{} | |
Expect(json.Unmarshal(rec.Body.Bytes(), &response)).To(BeNil()) | |
result := map[string]string{} | |
if links, ok := response["links"].(map[string]interface{}); ok { | |
if first, ok := links["first"]; ok { | |
result["first"] = first.(string) | |
Expect(err).ToNot(HaveOccurred()) | |
} | |
if next, ok := links["next"]; ok { | |
result["next"] = next.(string) | |
Expect(err).ToNot(HaveOccurred()) | |
} | |
if prev, ok := links["prev"]; ok { | |
result["prev"] = prev.(string) | |
Expect(err).ToNot(HaveOccurred()) | |
} | |
if last, ok := links["last"]; ok { | |
result["last"] = last.(string) | |
Expect(err).ToNot(HaveOccurred()) | |
} | |
} | |
return result | |
} | |
Context("number & size links", func() { | |
It("No prev and first on first page, size = 1", func() { | |
links := doRequest("/v1/posts?page[number]=1&page[size]=1") | |
Expect(links).To(HaveLen(2)) | |
Expect(links["next"]).To(Equal("/v1/posts?page[number]=2&page[size]=1")) | |
Expect(links["last"]).To(Equal("/v1/posts?page[number]=7&page[size]=1")) | |
}) | |
It("No prev and first on first page, size = 2", func() { | |
links := doRequest("/v1/posts?page[number]=1&page[size]=2") | |
Expect(links).To(HaveLen(2)) | |
Expect(links["next"]).To(Equal("/v1/posts?page[number]=2&page[size]=2")) | |
Expect(links["last"]).To(Equal("/v1/posts?page[number]=4&page[size]=2")) | |
}) | |
It("All links on page 2, size = 2", func() { | |
links := doRequest("/v1/posts?page[number]=2&page[size]=2") | |
Expect(links).To(HaveLen(4)) | |
Expect(links["first"]).To(Equal("/v1/posts?page[number]=1&page[size]=2")) | |
Expect(links["prev"]).To(Equal("/v1/posts?page[number]=1&page[size]=2")) | |
Expect(links["next"]).To(Equal("/v1/posts?page[number]=3&page[size]=2")) | |
Expect(links["last"]).To(Equal("/v1/posts?page[number]=4&page[size]=2")) | |
}) | |
It("No next and last on last page, size = 2", func() { | |
links := doRequest("/v1/posts?page[number]=4&page[size]=2") | |
Expect(links).To(HaveLen(2)) | |
Expect(links["prev"]).To(Equal("/v1/posts?page[number]=3&page[size]=2")) | |
Expect(links["first"]).To(Equal("/v1/posts?page[number]=1&page[size]=2")) | |
}) | |
It("Does not generate links if results fit on one page", func() { | |
links := doRequest("/v1/posts?page[number]=1&page[size]=10") | |
Expect(links).To(HaveLen(0)) | |
}) | |
}) | |
// If the combination of parameters is invalid, no links are generated and the normal FindAll method get's called | |
Context("invalid parameter combinations", func() { | |
It("all 4 of them", func() { | |
links := doRequest("/v1/posts?page[number]=1&page[size]=1&page[offset]=1&page[limit]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("number only", func() { | |
links := doRequest("/v1/posts?page[number]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("size only", func() { | |
links := doRequest("/v1/posts?page[size]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("offset only", func() { | |
links := doRequest("/v1/posts?page[offset]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("limit only", func() { | |
links := doRequest("/v1/posts?page[limit]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("number, size & offset", func() { | |
links := doRequest("/v1/posts?page[number]=1&page[size]=1&page[offset]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("number, size & limit", func() { | |
links := doRequest("/v1/posts?page[number]=1&page[size]=1&page[limit]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("limit, offset & number", func() { | |
links := doRequest("/v1/posts?page[limit]=1&page[offset]=1&page[number]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
It("limit, offset & size", func() { | |
links := doRequest("/v1/posts?page[limit]=1&page[offset]=1&page[size]=1") | |
Expect(links).To(HaveLen(0)) | |
}) | |
}) | |
Context("offset & limit links", func() { | |
It("No prev and first on offset = 0, limit = 1", func() { | |
links := doRequest("/v1/posts?page[offset]=0&page[limit]=1") | |
Expect(links).To(HaveLen(2)) | |
Expect(links["next"]).To(Equal("/v1/posts?page[limit]=1&page[offset]=1")) | |
Expect(links["last"]).To(Equal("/v1/posts?page[limit]=1&page[offset]=6")) | |
}) | |
It("No prev and first on offset = 0, limit = 2", func() { | |
links := doRequest("/v1/posts?page[offset]=0&page[limit]=2") | |
Expect(links).To(HaveLen(2)) | |
Expect(links["next"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=2")) | |
Expect(links["last"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=5")) | |
}) | |
It("All links on offset = 2, limit = 2", func() { | |
links := doRequest("/v1/posts?page[offset]=2&page[limit]=2") | |
Expect(links).To(HaveLen(4)) | |
Expect(links["first"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=0")) | |
Expect(links["prev"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=0")) | |
Expect(links["next"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=4")) | |
Expect(links["last"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=5")) | |
}) | |
It("No next and last on offset = 5, limit = 2", func() { | |
links := doRequest("/v1/posts?page[offset]=5&page[limit]=2") | |
Expect(links).To(HaveLen(2)) | |
Expect(links["prev"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=3")) | |
Expect(links["first"]).To(Equal("/v1/posts?page[limit]=2&page[offset]=0")) | |
}) | |
It("Does not generate links if results fit on one page", func() { | |
links := doRequest("/v1/posts?page[offset]=0&page[limit]=10") | |
Expect(links).To(HaveLen(0)) | |
}) | |
}) | |
Context("error codes", func() { | |
It("Should return the correct header on method not allowed", func() { | |
reqBody := strings.NewReader("") | |
req, err := http.NewRequest("PATCH", "/v1/posts", reqBody) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
expected := `{"errors":[{"status":"405","title":"Method Not Allowed"}]}` | |
Expect(rec.Body.String()).To(MatchJSON(expected)) | |
Expect(rec.Header().Get("Content-Type")).To(Equal(defaultContentTypHeader)) | |
Expect(rec.Code).To(Equal(http.StatusMethodNotAllowed)) | |
}) | |
}) | |
Context("add resource panics with invalid resources", func() { | |
It("Should really panic", func() { | |
api := NewAPI("blub") | |
invalidDataStructure := new(invalid) | |
testFunc := func() { | |
api.AddResource(*invalidDataStructure, &userSource{}) | |
} | |
Expect(testFunc).To(Panic()) | |
}) | |
}) | |
Context("test utility function getPointerToStruct", func() { | |
type someStruct struct { | |
someEntry string | |
} | |
It("Should work as expected", func() { | |
testItem := someStruct{} | |
actual := getPointerToStruct(testItem) | |
Expect(&testItem).To(Equal(actual)) | |
}) | |
It("should not fail when using a pointer", func() { | |
testItem := &someStruct{} | |
actual := getPointerToStruct(testItem) | |
Expect(&testItem).To(Equal(actual)) | |
}) | |
}) | |
}) | |
Context("When using middleware", func() { | |
var ( | |
api *API | |
rec *httptest.ResponseRecorder | |
source *fixtureSource | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
}, false} | |
api = NewAPI("v1") | |
api.AddResource(Post{}, source) | |
MiddleTest := func(c APIContexter, w http.ResponseWriter, r *http.Request) { | |
w.Header().Add("x-test", "test123") | |
} | |
api.UseMiddleware(MiddleTest) | |
rec = httptest.NewRecorder() | |
}) | |
It("Should call the middleware and set value", func() { | |
rec = httptest.NewRecorder() | |
req, err := http.NewRequest("OPTIONS", "/v1/posts", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Header().Get("x-test")).To(Equal("test123")) | |
}) | |
}) | |
Context("Custom context", func() { | |
var ( | |
api *API | |
customContextCalled bool = false | |
rec *httptest.ResponseRecorder | |
source *fixtureSource | |
) | |
type CustomContext struct { | |
APIContext | |
} | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
}, false} | |
api = NewAPI("v1") | |
api.AddResource(Post{}, source) | |
api.SetContextAllocator(func(api *API) APIContexter { | |
customContextCalled = true | |
return &CustomContext{} | |
}) | |
rec = httptest.NewRecorder() | |
}) | |
It("calls into custom context allocator", func() { | |
rec = httptest.NewRecorder() | |
req, err := http.NewRequest("OPTIONS", "/v1/posts", nil) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(customContextCalled).To(BeTrue()) | |
}) | |
}) | |
Context("dynamic baseurl handling", func() { | |
var ( | |
api *API | |
rec *httptest.ResponseRecorder | |
source *fixtureSource | |
) | |
BeforeEach(func() { | |
source = &fixtureSource{map[string]*Post{ | |
"1": {ID: "1", Title: "Hello, World!"}, | |
}, false} | |
marshalers := map[string]ContentMarshaler{ | |
`application/vnd.api+json`: JSONContentMarshaler{}, | |
} | |
api = NewAPIWithMarshalling("/secret/", &requestURLResolver{}, marshalers) | |
api.AddResource(Post{}, source) | |
rec = httptest.NewRecorder() | |
}) | |
It("should change dependening on request header in FindAll", func() { | |
firstURI := "https://god-mode.example.com" | |
secondURI := "https://top-secret.example.com" | |
req, err := http.NewRequest("GET", "/secret/posts", nil) | |
req.Header.Set("REQUEST_URI", firstURI) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(err).ToNot(HaveOccurred()) | |
Expect(rec.Body.Bytes()).To(ContainSubstring(firstURI)) | |
Expect(rec.Body.Bytes()).ToNot(ContainSubstring(secondURI)) | |
rec = httptest.NewRecorder() | |
req2, err := http.NewRequest("GET", "/secret/posts", nil) | |
req2.Header.Set("REQUEST_URI", secondURI) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req2) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(err).ToNot(HaveOccurred()) | |
Expect(rec.Body.Bytes()).To(ContainSubstring(secondURI)) | |
Expect(rec.Body.Bytes()).ToNot(ContainSubstring(firstURI)) | |
}) | |
It("should change dependening on request header in FindOne", func() { | |
expected := "https://god-mode.example.com" | |
req, err := http.NewRequest("GET", "/secret/posts/1", nil) | |
req.Header.Set("REQUEST_URI", expected) | |
Expect(err).To(BeNil()) | |
api.Handler().ServeHTTP(rec, req) | |
Expect(rec.Code).To(Equal(http.StatusOK)) | |
Expect(err).ToNot(HaveOccurred()) | |
Expect(rec.Body.Bytes()).To(ContainSubstring(expected)) | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment