jsonapi/response_test.go

1565 lines
36 KiB
Go

package jsonapi
import (
"bytes"
"encoding/json"
"reflect"
"sort"
"testing"
"time"
)
func TestMarshalPayload(t *testing.T) {
book := &Book{ID: 1}
books := []*Book{book, &Book{ID: 2}}
var jsonData map[string]interface{}
// One
out1 := bytes.NewBuffer(nil)
MarshalPayload(out1, book)
if err := json.Unmarshal(out1.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
if _, ok := jsonData["data"].(map[string]interface{}); !ok {
t.Fatalf("data key did not contain an Hash/Dict/Map")
}
// Many
out2 := bytes.NewBuffer(nil)
MarshalPayload(out2, books)
if err := json.Unmarshal(out2.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
if _, ok := jsonData["data"].([]interface{}); !ok {
t.Fatalf("data key did not contain an Array")
}
}
func TestMarshal_attrStringSlice(t *testing.T) {
tags := []string{"fiction", "sale"}
b := &Book{ID: 1, Tags: tags}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, b); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
jsonTags := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{})["tags"].([]interface{})
if e, a := len(tags), len(jsonTags); e != a {
t.Fatalf("Was expecting tags of length %d got %d", e, a)
}
// Convert from []interface{} to []string
jsonTagsStrings := []string{}
for _, tag := range jsonTags {
jsonTagsStrings = append(jsonTagsStrings, tag.(string))
}
// Sort both
sort.Strings(jsonTagsStrings)
sort.Strings(tags)
for i, tag := range tags {
if e, a := tag, jsonTagsStrings[i]; e != a {
t.Fatalf("At index %d, was expecting %s got %s", i, e, a)
}
}
}
func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) {
blog := &Blog{}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, blog); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{})
// Verifiy the "posts" relation was an empty array
posts, ok := relationships["posts"]
if !ok {
t.Fatal("Was expecting the data.relationships.posts key/value to have been present")
}
postsMap, ok := posts.(map[string]interface{})
if !ok {
t.Fatal("data.relationships.posts was not a map")
}
postsData, ok := postsMap["data"]
if !ok {
t.Fatal("Was expecting the data.relationships.posts.data key/value to have been present")
}
postsDataSlice, ok := postsData.([]interface{})
if !ok {
t.Fatal("data.relationships.posts.data was not a slice []")
}
if len(postsDataSlice) != 0 {
t.Fatal("Was expecting the data.relationships.posts.data value to have been an empty array []")
}
// Verifiy the "current_post" was a null
currentPost, postExists := relationships["current_post"]
if !postExists {
t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted")
}
currentPostMap, ok := currentPost.(map[string]interface{})
if !ok {
t.Fatal("data.relationships.current_post was not a map")
}
currentPostData, ok := currentPostMap["data"]
if !ok {
t.Fatal("Was expecting the data.relationships.current_post.data key/value to have been present")
}
if currentPostData != nil {
t.Fatal("Was expecting the data.relationships.current_post.data value to have been nil/null")
}
}
func TestWithOmitsEmptyAnnotationOnRelation(t *testing.T) {
type BlogOptionalPosts struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts,omitempty"`
CurrentPost *Post `jsonapi:"relation,current_post,omitempty"`
}
blog := &BlogOptionalPosts{ID: 999}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, blog); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
payload := jsonData["data"].(map[string]interface{})
// Verify relationship was NOT set
if val, exists := payload["relationships"]; exists {
t.Fatalf("Was expecting the data.relationships key/value to have been empty - it was not and had a value of %v", val)
}
}
func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) {
type BlogOptionalPosts struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts,omitempty"`
CurrentPost *Post `jsonapi:"relation,current_post,omitempty"`
}
blog := &BlogOptionalPosts{
ID: 999,
CurrentPost: &Post{
ID: 123,
},
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, blog); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
payload := jsonData["data"].(map[string]interface{})
// Verify relationship was set
if _, exists := payload["relationships"]; !exists {
t.Fatal("Was expecting the data.relationships key/value to have NOT been empty")
}
relationships := payload["relationships"].(map[string]interface{})
// Verify the relationship was not omitted, and is not null
if val, exists := relationships["current_post"]; !exists {
t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted")
} else if val.(map[string]interface{})["data"] == nil {
t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null")
}
}
func TestMarshalIDPtr(t *testing.T) {
id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang"
car := &Car{
ID: &id,
Make: &make,
Model: &model,
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, car); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
data := jsonData["data"].(map[string]interface{})
// attributes := data["attributes"].(map[string]interface{})
// Verify that the ID was sent
val, exists := data["id"]
if !exists {
t.Fatal("Was expecting the data.id member to exist")
}
if val != id {
t.Fatalf("Was expecting the data.id member to be `%s`, got `%s`", id, val)
}
}
func TestMarshalOnePayload_omitIDString(t *testing.T) {
type Foo struct {
ID string `jsonapi:"primary,foo"`
Title string `jsonapi:"attr,title"`
}
foo := &Foo{Title: "Foo"}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, foo); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
payload := jsonData["data"].(map[string]interface{})
// Verify that empty ID of type string gets omitted. See:
// https://github.com/google/jsonapi/issues/83#issuecomment-285611425
_, ok := payload["id"]
if ok {
t.Fatal("Was expecting the data.id member to be omitted")
}
}
func TestMarshall_invalidIDType(t *testing.T) {
type badIDStruct struct {
ID *bool `jsonapi:"primary,cars"`
}
id := true
o := &badIDStruct{ID: &id}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, o); err != ErrBadJSONAPIID {
t.Fatalf(
"Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err,
)
}
}
func TestOmitsEmptyAnnotation(t *testing.T) {
book := &Book{
Author: "aren55555",
PublishedAt: time.Now().AddDate(0, -1, 0),
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, book); err != nil {
t.Fatal(err)
}
var jsonData map[string]interface{}
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
t.Fatal(err)
}
attributes := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{})
// Verify that the specifically omitted field were omitted
if val, exists := attributes["title"]; exists {
t.Fatalf("Was expecting the data.attributes.title key/value to have been omitted - it was not and had a value of %v", val)
}
if val, exists := attributes["pages"]; exists {
t.Fatalf("Was expecting the data.attributes.pages key/value to have been omitted - it was not and had a value of %v", val)
}
// Verify the implicity omitted fields were omitted
if val, exists := attributes["PublishedAt"]; exists {
t.Fatalf("Was expecting the data.attributes.PublishedAt key/value to have been implicity omitted - it was not and had a value of %v", val)
}
// Verify the unset fields were not omitted
if _, exists := attributes["isbn"]; !exists {
t.Fatal("Was expecting the data.attributes.isbn key/value to have NOT been omitted")
}
}
func TestHasPrimaryAnnotation(t *testing.T) {
testModel := &Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Now(),
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Type != "blogs" {
t.Fatalf("type should have been blogs, got %s", data.Type)
}
if data.ID != "5" {
t.Fatalf("ID not transfered")
}
}
func TestSupportsAttributes(t *testing.T) {
testModel := &Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Now(),
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Attributes == nil {
t.Fatalf("Expected attributes")
}
if data.Attributes["title"] != "Title 1" {
t.Fatalf("Attributes hash not populated using tags correctly")
}
}
func TestOmitsZeroTimes(t *testing.T) {
testModel := &Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Time{},
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Attributes == nil {
t.Fatalf("Expected attributes")
}
if data.Attributes["created_at"] != nil {
t.Fatalf("Created at was serialized even though it was a zero Time")
}
}
func TestMarshalISO8601Time(t *testing.T) {
testModel := &Timestamp{
ID: 5,
Time: time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC),
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Attributes == nil {
t.Fatalf("Expected attributes")
}
if data.Attributes["timestamp"] != "2016-08-17T08:27:12Z" {
t.Fatal("Timestamp was not serialised into ISO8601 correctly")
}
}
func TestMarshalISO8601TimePointer(t *testing.T) {
tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
testModel := &Timestamp{
ID: 5,
Next: &tm,
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Attributes == nil {
t.Fatalf("Expected attributes")
}
if data.Attributes["next"] != "2016-08-17T08:27:12Z" {
t.Fatal("Next was not serialised into ISO8601 correctly")
}
}
func TestSupportsLinkable(t *testing.T) {
testModel := &Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Now(),
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Links == nil {
t.Fatal("Expected data.links")
}
links := *data.Links
self, hasSelf := links["self"]
if !hasSelf {
t.Fatal("Expected 'self' link to be present")
}
if _, isString := self.(string); !isString {
t.Fatal("Expected 'self' to contain a string")
}
comments, hasComments := links["comments"]
if !hasComments {
t.Fatal("expect 'comments' to be present")
}
commentsMap, isMap := comments.(map[string]interface{})
if !isMap {
t.Fatal("Expected 'comments' to contain a map")
}
commentsHref, hasHref := commentsMap["href"]
if !hasHref {
t.Fatal("Expect 'comments' to contain an 'href' key/value")
}
if _, isString := commentsHref.(string); !isString {
t.Fatal("Expected 'href' to contain a string")
}
commentsMeta, hasMeta := commentsMap["meta"]
if !hasMeta {
t.Fatal("Expect 'comments' to contain a 'meta' key/value")
}
commentsMetaMap, isMap := commentsMeta.(map[string]interface{})
if !isMap {
t.Fatal("Expected 'comments' to contain a map")
}
commentsMetaObject := Meta(commentsMetaMap)
countsMap, isMap := commentsMetaObject["counts"].(map[string]interface{})
if !isMap {
t.Fatal("Expected 'counts' to contain a map")
}
for k, v := range countsMap {
if _, isNum := v.(float64); !isNum {
t.Fatalf("Exepected value at '%s' to be a numeric (float64)", k)
}
}
}
func TestInvalidLinkable(t *testing.T) {
testModel := &BadComment{
ID: 5,
Body: "Hello World",
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err == nil {
t.Fatal("Was expecting an error")
}
}
func TestSupportsMetable(t *testing.T) {
testModel := &Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Now(),
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
data := resp.Data
if data.Meta == nil {
t.Fatalf("Expected data.meta")
}
meta := Meta(*data.Meta)
if e, a := "extra details regarding the blog", meta["detail"]; e != a {
t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a)
}
}
func TestRelations(t *testing.T) {
testModel := testBlog()
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
relations := resp.Data.Relationships
if relations == nil {
t.Fatalf("Relationships were not materialized")
}
if relations["posts"] == nil {
t.Fatalf("Posts relationship was not materialized")
} else {
if relations["posts"].(map[string]interface{})["links"] == nil {
t.Fatalf("Posts relationship links were not materialized")
}
if relations["posts"].(map[string]interface{})["meta"] == nil {
t.Fatalf("Posts relationship meta were not materialized")
}
}
if relations["current_post"] == nil {
t.Fatalf("Current post relationship was not materialized")
} else {
if relations["current_post"].(map[string]interface{})["links"] == nil {
t.Fatalf("Current post relationship links were not materialized")
}
if relations["current_post"].(map[string]interface{})["meta"] == nil {
t.Fatalf("Current post relationship meta were not materialized")
}
}
if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 {
t.Fatalf("Did not materialize two posts")
}
}
func TestNoRelations(t *testing.T) {
testModel := &Blog{ID: 1, Title: "Title 1", CreatedAt: time.Now()}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
if resp.Included != nil {
t.Fatalf("Encoding json response did not omit included")
}
}
func TestMarshalPayloadWithoutIncluded(t *testing.T) {
data := &Post{
ID: 1,
BlogID: 2,
ClientID: "123e4567-e89b-12d3-a456-426655440000",
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 20,
Body: "First",
},
&Comment{
ID: 21,
Body: "Hello World",
},
},
LatestComment: &Comment{
ID: 22,
Body: "Cool!",
},
}
out := bytes.NewBuffer(nil)
if err := MarshalPayloadWithoutIncluded(out, data); err != nil {
t.Fatal(err)
}
resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
if resp.Included != nil {
t.Fatalf("Encoding json response did not omit included")
}
}
func TestMarshalPayload_many(t *testing.T) {
data := []interface{}{
&Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Now(),
Posts: []*Post{
&Post{
ID: 1,
Title: "Foo",
Body: "Bar",
},
&Post{
ID: 2,
Title: "Fuubar",
Body: "Bas",
},
},
CurrentPost: &Post{
ID: 1,
Title: "Foo",
Body: "Bar",
},
},
&Blog{
ID: 6,
Title: "Title 2",
CreatedAt: time.Now(),
Posts: []*Post{
&Post{
ID: 3,
Title: "Foo",
Body: "Bar",
},
&Post{
ID: 4,
Title: "Fuubar",
Body: "Bas",
},
},
CurrentPost: &Post{
ID: 4,
Title: "Foo",
Body: "Bar",
},
},
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, data); err != nil {
t.Fatal(err)
}
resp := new(ManyPayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
d := resp.Data
if len(d) != 2 {
t.Fatalf("data should have two elements")
}
}
func TestMarshalMany_WithSliceOfStructPointers(t *testing.T) {
var data []*Blog
for len(data) < 2 {
data = append(data, testBlog())
}
out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, data); err != nil {
t.Fatal(err)
}
resp := new(ManyPayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
d := resp.Data
if len(d) != 2 {
t.Fatalf("data should have two elements")
}
}
func TestMarshalManyWithoutIncluded(t *testing.T) {
var data []*Blog
for len(data) < 2 {
data = append(data, testBlog())
}
out := bytes.NewBuffer(nil)
if err := MarshalPayloadWithoutIncluded(out, data); err != nil {
t.Fatal(err)
}
resp := new(ManyPayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}
d := resp.Data
if len(d) != 2 {
t.Fatalf("data should have two elements")
}
if resp.Included != nil {
t.Fatalf("Encoding json response did not omit included")
}
}
func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) {
structs := []*Book{
&Book{ID: 1, Author: "aren55555", ISBN: "abc"},
&Book{ID: 2, Author: "shwoodard", ISBN: "xyz"},
}
interfaces := []interface{}{}
for _, s := range structs {
interfaces = append(interfaces, s)
}
// Perform Marshals
structsOut := new(bytes.Buffer)
if err := MarshalPayload(structsOut, structs); err != nil {
t.Fatal(err)
}
interfacesOut := new(bytes.Buffer)
if err := MarshalPayload(interfacesOut, interfaces); err != nil {
t.Fatal(err)
}
// Generic JSON Unmarshal
structsData, interfacesData :=
make(map[string]interface{}), make(map[string]interface{})
if err := json.Unmarshal(structsOut.Bytes(), &structsData); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(interfacesOut.Bytes(), &interfacesData); err != nil {
t.Fatal(err)
}
// Compare Result
if !reflect.DeepEqual(structsData, interfacesData) {
t.Fatal("Was expecting the JSON API generated to be the same")
}
}
func TestMarshal_InvalidIntefaceArgument(t *testing.T) {
out := new(bytes.Buffer)
if err := MarshalPayload(out, true); err != ErrUnexpectedType {
t.Fatal("Was expecting an error")
}
if err := MarshalPayload(out, 25); err != ErrUnexpectedType {
t.Fatal("Was expecting an error")
}
if err := MarshalPayload(out, Book{}); err != ErrUnexpectedType {
t.Fatal("Was expecting an error")
}
}
func TestMergeNode(t *testing.T) {
parent := &Node{
Type: "Good",
ID: "99",
Attributes: map[string]interface{}{"fizz": "buzz"},
}
child := &Node{
Type: "Better",
ClientID: "1111",
Attributes: map[string]interface{}{"timbuk": 2},
}
expected := &Node{
Type: "Better",
ID: "99",
ClientID: "1111",
Attributes: map[string]interface{}{"fizz": "buzz", "timbuk": 2},
}
parent.merge(child)
if !reflect.DeepEqual(expected, parent) {
t.Errorf("Got %+v Expected %+v", parent, expected)
}
}
func TestIsEmbeddedStruct(t *testing.T) {
type foo struct{}
structType := reflect.TypeOf(foo{})
stringType := reflect.TypeOf("")
if structType.Kind() != reflect.Struct {
t.Fatal("structType.Kind() is not a struct.")
}
if stringType.Kind() != reflect.String {
t.Fatal("stringType.Kind() is not a string.")
}
type test struct {
scenario string
input reflect.StructField
expectedRes bool
}
tests := []test{
test{
scenario: "success",
input: reflect.StructField{Anonymous: true, Type: structType},
expectedRes: true,
},
test{
scenario: "wrong type",
input: reflect.StructField{Anonymous: true, Type: stringType},
expectedRes: false,
},
test{
scenario: "not embedded",
input: reflect.StructField{Type: structType},
expectedRes: false,
},
}
for _, test := range tests {
res := isEmbeddedStruct(test.input)
if res != test.expectedRes {
t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes)
}
}
}
func TestShouldIgnoreField(t *testing.T) {
type test struct {
scenario string
input string
expectedRes bool
}
tests := []test{
test{
scenario: "opt-out",
input: annotationIgnore,
expectedRes: true,
},
test{
scenario: "no tag",
input: "",
expectedRes: false,
},
test{
scenario: "wrong tag",
input: "wrong,tag",
expectedRes: false,
},
}
for _, test := range tests {
res := shouldIgnoreField(test.input)
if res != test.expectedRes {
t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes)
}
}
}
func TestIsValidEmbeddedStruct(t *testing.T) {
type foo struct{}
structType := reflect.TypeOf(foo{})
stringType := reflect.TypeOf("")
if structType.Kind() != reflect.Struct {
t.Fatal("structType.Kind() is not a struct.")
}
if stringType.Kind() != reflect.String {
t.Fatal("stringType.Kind() is not a string.")
}
type test struct {
scenario string
input reflect.StructField
expectedRes bool
}
tests := []test{
test{
scenario: "success",
input: reflect.StructField{Anonymous: true, Type: structType},
expectedRes: true,
},
test{
scenario: "opt-out",
input: reflect.StructField{Anonymous: true, Tag: "jsonapi:\"-\"", Type: structType},
expectedRes: false,
},
test{
scenario: "wrong type",
input: reflect.StructField{Anonymous: true, Type: stringType},
expectedRes: false,
},
test{
scenario: "not embedded",
input: reflect.StructField{Type: structType},
expectedRes: false,
},
}
for _, test := range tests {
res := (isEmbeddedStruct(test.input) && !shouldIgnoreField(test.input.Tag.Get(annotationJSONAPI)))
if res != test.expectedRes {
t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes)
}
}
}
// TestEmbeddedUnmarshalOrder tests the behavior of the marshaler/unmarshaler of embedded structs
// when a struct has an embedded struct w/ competing attributes, the top-level attributes take precedence
// it compares the behavior against the standard json package
func TestEmbeddedUnmarshalOrder(t *testing.T) {
type Bar struct {
Name int `jsonapi:"attr,Name"`
}
type Foo struct {
Bar
ID string `jsonapi:"primary,foos"`
Name string `jsonapi:"attr,Name"`
}
f := &Foo{
ID: "1",
Name: "foo",
Bar: Bar{
Name: 5,
},
}
// marshal f (Foo) using jsonapi marshaler
jsonAPIData := bytes.NewBuffer(nil)
if err := MarshalPayload(jsonAPIData, f); err != nil {
t.Fatal(err)
}
// marshal f (Foo) using json marshaler
jsonData, err := json.Marshal(f)
// convert bytes to map[string]interface{} so that we can do a semantic JSON comparison
var jsonAPIVal, jsonVal map[string]interface{}
if err := json.Unmarshal(jsonAPIData.Bytes(), &jsonAPIVal); err != nil {
t.Fatal(err)
}
if err = json.Unmarshal(jsonData, &jsonVal); err != nil {
t.Fatal(err)
}
// get to the jsonapi attribute map
jAttrMap := jsonAPIVal["data"].(map[string]interface{})["attributes"].(map[string]interface{})
// compare
if !reflect.DeepEqual(jAttrMap["Name"], jsonVal["Name"]) {
t.Errorf("Got\n%s\nExpected\n%s\n", jAttrMap["Name"], jsonVal["Name"])
}
}
// TestEmbeddedMarshalOrder tests the behavior of the marshaler/unmarshaler of embedded structs
// when a struct has an embedded struct w/ competing attributes, the top-level attributes take precedence
// it compares the behavior against the standard json package
func TestEmbeddedMarshalOrder(t *testing.T) {
type Bar struct {
Name int `jsonapi:"attr,Name"`
}
type Foo struct {
Bar
ID string `jsonapi:"primary,foos"`
Name string `jsonapi:"attr,Name"`
}
// get a jsonapi payload w/ Name attribute of an int type
payloadWithInt, err := json.Marshal(&OnePayload{
Data: &Node{
Type: "foos",
ID: "1",
Attributes: map[string]interface{}{
"Name": 5,
},
},
})
if err != nil {
t.Fatal(err)
}
// get a jsonapi payload w/ Name attribute of an string type
payloadWithString, err := json.Marshal(&OnePayload{
Data: &Node{
Type: "foos",
ID: "1",
Attributes: map[string]interface{}{
"Name": "foo",
},
},
})
if err != nil {
t.Fatal(err)
}
// unmarshal payloadWithInt to f (Foo) using jsonapi unmarshaler; expecting an error
f := &Foo{}
if err := UnmarshalPayload(bytes.NewReader(payloadWithInt), f); err == nil {
t.Errorf("expected an error: int value of 5 should attempt to map to Foo.Name (string) and error")
}
// unmarshal payloadWithString to f (Foo) using jsonapi unmarshaler; expecting no error
f = &Foo{}
if err := UnmarshalPayload(bytes.NewReader(payloadWithString), f); err != nil {
t.Error(err)
}
if f.Name != "foo" {
t.Errorf("Got\n%s\nExpected\n%s\n", "foo", f.Name)
}
// get a json payload w/ Name attribute of an int type
bWithInt, err := json.Marshal(map[string]interface{}{
"Name": 5,
})
if err != nil {
t.Fatal(err)
}
// get a json payload w/ Name attribute of an string type
bWithString, err := json.Marshal(map[string]interface{}{
"Name": "foo",
})
if err != nil {
t.Fatal(err)
}
// unmarshal bWithInt to f (Foo) using json unmarshaler; expecting an error
f = &Foo{}
if err := json.Unmarshal(bWithInt, f); err == nil {
t.Errorf("expected an error: int value of 5 should attempt to map to Foo.Name (string) and error")
}
// unmarshal bWithString to f (Foo) using json unmarshaler; expecting no error
f = &Foo{}
if err := json.Unmarshal(bWithString, f); err != nil {
t.Error(err)
}
if f.Name != "foo" {
t.Errorf("Got\n%s\nExpected\n%s\n", "foo", f.Name)
}
}
func TestMarshalUnmarshalCompositeStruct(t *testing.T) {
type Thing struct {
ID int `jsonapi:"primary,things"`
Fizz string `jsonapi:"attr,fizz"`
Buzz int `jsonapi:"attr,buzz"`
}
type Model struct {
Thing
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
}
type test struct {
name string
payload *OnePayload
dst, expected interface{}
}
scenarios := []test{}
scenarios = append(scenarios, test{
name: "Model embeds Thing, models have no annotation overlaps",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "things",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"buzz": 99,
"fizz": "fizzy",
"foo": "fooey",
},
},
},
expected: &Model{
Foo: "fooey",
Bar: "barry",
Bat: "batty",
Thing: Thing{
ID: 1,
Fizz: "fizzy",
Buzz: 99,
},
},
})
{
type Model struct {
Thing
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
Buzz int `jsonapi:"attr,buzz"` // overrides Thing.Buzz
}
scenarios = append(scenarios, test{
name: "Model embeds Thing, overlap Buzz attribute",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "things",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"buzz": 99,
"fizz": "fizzy",
"foo": "fooey",
},
},
},
expected: &Model{
Foo: "fooey",
Bar: "barry",
Bat: "batty",
Buzz: 99,
Thing: Thing{
ID: 1,
Fizz: "fizzy",
},
},
})
}
{
type Model struct {
Thing
ModelID int `jsonapi:"primary,models"` //overrides Thing.ID due to primary annotation
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
Buzz int `jsonapi:"attr,buzz"` // overrides Thing.Buzz
}
scenarios = append(scenarios, test{
name: "Model embeds Thing, attribute, and primary annotation overlap",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "models",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"buzz": 99,
"fizz": "fizzy",
"foo": "fooey",
},
},
},
expected: &Model{
ModelID: 1,
Foo: "fooey",
Bar: "barry",
Bat: "batty",
Buzz: 99,
Thing: Thing{
Fizz: "fizzy",
},
},
})
}
{
type Model struct {
Thing `jsonapi:"-"`
ModelID int `jsonapi:"primary,models"`
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
Buzz int `jsonapi:"attr,buzz"`
}
scenarios = append(scenarios, test{
name: "Model embeds Thing, but is annotated w/ ignore",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "models",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"buzz": 99,
"foo": "fooey",
},
},
},
expected: &Model{
ModelID: 1,
Foo: "fooey",
Bar: "barry",
Bat: "batty",
Buzz: 99,
},
})
}
{
type Model struct {
*Thing
ModelID int `jsonapi:"primary,models"`
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
}
scenarios = append(scenarios, test{
name: "Model embeds pointer of Thing; Thing is initialized in advance",
dst: &Model{Thing: &Thing{}},
payload: &OnePayload{
Data: &Node{
Type: "models",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"foo": "fooey",
"buzz": 99,
"fizz": "fizzy",
},
},
},
expected: &Model{
Thing: &Thing{
Fizz: "fizzy",
Buzz: 99,
},
ModelID: 1,
Foo: "fooey",
Bar: "barry",
Bat: "batty",
},
})
}
{
type Model struct {
*Thing
ModelID int `jsonapi:"primary,models"`
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
}
scenarios = append(scenarios, test{
name: "Model embeds pointer of Thing; Thing is initialized w/ Unmarshal",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "models",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"foo": "fooey",
"buzz": 99,
"fizz": "fizzy",
},
},
},
expected: &Model{
Thing: &Thing{
Fizz: "fizzy",
Buzz: 99,
},
ModelID: 1,
Foo: "fooey",
Bar: "barry",
Bat: "batty",
},
})
}
{
type Model struct {
*Thing
ModelID int `jsonapi:"primary,models"`
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
}
scenarios = append(scenarios, test{
name: "Model embeds pointer of Thing; jsonapi model doesn't assign anything to Thing; *Thing is nil",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "models",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"foo": "fooey",
},
},
},
expected: &Model{
ModelID: 1,
Foo: "fooey",
Bar: "barry",
Bat: "batty",
},
})
}
{
type Model struct {
*Thing
ModelID int `jsonapi:"primary,models"`
Foo string `jsonapi:"attr,foo"`
Bar string `jsonapi:"attr,bar"`
Bat string `jsonapi:"attr,bat"`
}
scenarios = append(scenarios, test{
name: "Model embeds pointer of Thing; *Thing is nil",
dst: &Model{},
payload: &OnePayload{
Data: &Node{
Type: "models",
ID: "1",
Attributes: map[string]interface{}{
"bar": "barry",
"bat": "batty",
"foo": "fooey",
},
},
},
expected: &Model{
ModelID: 1,
Foo: "fooey",
Bar: "barry",
Bat: "batty",
},
})
}
for _, scenario := range scenarios {
t.Logf("running scenario: %s\n", scenario.name)
// get the expected model and marshal to jsonapi
buf := bytes.NewBuffer(nil)
if err := MarshalPayload(buf, scenario.expected); err != nil {
t.Fatal(err)
}
// get the node model representation and marshal to jsonapi
payload, err := json.Marshal(scenario.payload)
if err != nil {
t.Fatal(err)
}
// assert that we're starting w/ the same payload
isJSONEqual, err := isJSONEqual(payload, buf.Bytes())
if err != nil {
t.Fatal(err)
}
if !isJSONEqual {
t.Errorf("Got\n%s\nExpected\n%s\n", buf.Bytes(), payload)
}
// run jsonapi unmarshal
if err := UnmarshalPayload(bytes.NewReader(payload), scenario.dst); err != nil {
t.Fatal(err)
}
// assert decoded and expected models are equal
if !reflect.DeepEqual(scenario.expected, scenario.dst) {
t.Errorf("Got\n%#v\nExpected\n%#v\n", scenario.dst, scenario.expected)
}
}
}
func TestMarshal_duplicatePrimaryAnnotationFromEmbededStructs(t *testing.T) {
type Outer struct {
ID string `jsonapi:"primary,outer"`
Comment
*Post
}
o := Outer{
ID: "outer",
Comment: Comment{ID: 1},
Post: &Post{ID: 5},
}
var payloadData map[string]interface{}
// Test the standard libraries JSON handling of dup (ID) fields
jsonData, err := json.Marshal(o)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(jsonData, &payloadData); err != nil {
t.Fatal(err)
}
if e, a := o.ID, payloadData["ID"]; e != a {
t.Fatalf("Was expecting ID to be %v, got %v", e, a)
}
// Test the JSONAPI lib handling of dup (ID) fields
jsonAPIData := new(bytes.Buffer)
if err := MarshalPayload(jsonAPIData, &o); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil {
t.Fatal(err)
}
data := payloadData["data"].(map[string]interface{})
id := data["id"].(string)
if e, a := o.ID, id; e != a {
t.Fatalf("Was expecting ID to be %v, got %v", e, a)
}
}
func testBlog() *Blog {
return &Blog{
ID: 5,
Title: "Title 1",
CreatedAt: time.Now(),
Posts: []*Post{
&Post{
ID: 1,
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 1,
Body: "foo",
},
&Comment{
ID: 2,
Body: "bar",
},
},
LatestComment: &Comment{
ID: 1,
Body: "foo",
},
},
&Post{
ID: 2,
Title: "Fuubar",
Body: "Bas",
Comments: []*Comment{
&Comment{
ID: 1,
Body: "foo",
},
&Comment{
ID: 3,
Body: "bas",
},
},
LatestComment: &Comment{
ID: 1,
Body: "foo",
},
},
},
CurrentPost: &Post{
ID: 1,
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 1,
Body: "foo",
},
&Comment{
ID: 2,
Body: "bar",
},
},
LatestComment: &Comment{
ID: 1,
Body: "foo",
},
},
}
}
func isJSONEqual(b1, b2 []byte) (bool, error) {
var i1, i2 interface{}
var result bool
var err error
if err = json.Unmarshal(b1, &i1); err != nil {
return result, err
}
if err = json.Unmarshal(b2, &i2); err != nil {
return result, err
}
result = reflect.DeepEqual(i1, i2)
return result, err
}