1package jsonapi
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"io"
8	"reflect"
9	"sort"
10	"strings"
11	"testing"
12	"time"
13)
14
15func TestUnmarshall_attrStringSlice(t *testing.T) {
16	out := &Book{}
17	tags := []string{"fiction", "sale"}
18	data := map[string]interface{}{
19		"data": map[string]interface{}{
20			"type":       "books",
21			"id":         "1",
22			"attributes": map[string]interface{}{"tags": tags},
23		},
24	}
25	b, err := json.Marshal(data)
26	if err != nil {
27		t.Fatal(err)
28	}
29
30	if err := UnmarshalPayload(bytes.NewReader(b), out); err != nil {
31		t.Fatal(err)
32	}
33
34	if e, a := len(tags), len(out.Tags); e != a {
35		t.Fatalf("Was expecting %d tags, got %d", e, a)
36	}
37
38	sort.Strings(tags)
39	sort.Strings(out.Tags)
40
41	for i, tag := range tags {
42		if e, a := tag, out.Tags[i]; e != a {
43			t.Fatalf("At index %d, was expecting %s got %s", i, e, a)
44		}
45	}
46}
47
48func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
49	out := new(WithPointer)
50	in := map[string]interface{}{
51		"name":      "The name",
52		"is-active": true,
53		"int-val":   8,
54		"float-val": 1.1,
55	}
56	if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil {
57		t.Fatal(err)
58	}
59	if *out.Name != "The name" {
60		t.Fatalf("Error unmarshalling to string ptr")
61	}
62	if !*out.IsActive {
63		t.Fatalf("Error unmarshalling to bool ptr")
64	}
65	if *out.IntVal != 8 {
66		t.Fatalf("Error unmarshalling to int ptr")
67	}
68	if *out.FloatVal != 1.1 {
69		t.Fatalf("Error unmarshalling to float ptr")
70	}
71}
72
73func TestUnmarshalPayload_ptrsAllNil(t *testing.T) {
74	out := new(WithPointer)
75	if err := UnmarshalPayload(
76		strings.NewReader(`{"data": {}}`), out); err != nil {
77		t.Fatalf("Error unmarshalling to Foo")
78	}
79
80	if out.ID != nil {
81		t.Fatalf("Error unmarshalling; expected ID ptr to be nil")
82	}
83}
84
85func TestUnmarshalPayloadWithPointerID(t *testing.T) {
86	out := new(WithPointer)
87	attrs := map[string]interface{}{}
88
89	if err := UnmarshalPayload(sampleWithPointerPayload(attrs), out); err != nil {
90		t.Fatalf("Error unmarshalling to Foo")
91	}
92
93	// these were present in the payload -- expect val to be not nil
94	if out.ID == nil {
95		t.Fatalf("Error unmarshalling; expected ID ptr to be not nil")
96	}
97	if e, a := uint64(2), *out.ID; e != a {
98		t.Fatalf("Was expecting the ID to have a value of %d, got %d", e, a)
99	}
100}
101
102func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) {
103	out := new(WithPointer)
104	in := map[string]interface{}{
105		"name":      "The name",
106		"is-active": true,
107	}
108
109	if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil {
110		t.Fatalf("Error unmarshalling to Foo")
111	}
112
113	// these were present in the payload -- expect val to be not nil
114	if out.Name == nil || out.IsActive == nil {
115		t.Fatalf("Error unmarshalling; expected ptr to be not nil")
116	}
117
118	// these were absent in the payload -- expect val to be nil
119	if out.IntVal != nil || out.FloatVal != nil {
120		t.Fatalf("Error unmarshalling; expected ptr to be nil")
121	}
122}
123
124func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) {
125	out := new(WithPointer)
126	in := map[string]interface{}{
127		"name": true, // This is the wrong type.
128	}
129	expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`"
130
131	err := UnmarshalPayload(sampleWithPointerPayload(in), out)
132
133	if err == nil {
134		t.Fatalf("Expected error due to invalid type.")
135	}
136	if err.Error() != expectedErrorMessage {
137		t.Fatalf("Unexpected error message: %s", err.Error())
138	}
139	if _, ok := err.(ErrUnsupportedPtrType); !ok {
140		t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
141	}
142}
143
144func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) {
145	out := new(WithPointer)
146	in := map[string]interface{}{
147		"name": &map[string]interface{}{"a": 5}, // This is the wrong type.
148	}
149	expectedErrorMessage := "jsonapi: Can't unmarshal map[a:5] (map) to struct field `Name`, which is a pointer to `string`"
150
151	err := UnmarshalPayload(sampleWithPointerPayload(in), out)
152
153	if err == nil {
154		t.Fatalf("Expected error due to invalid type.")
155	}
156	if err.Error() != expectedErrorMessage {
157		t.Fatalf("Unexpected error message: %s", err.Error())
158	}
159	if _, ok := err.(ErrUnsupportedPtrType); !ok {
160		t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
161	}
162}
163
164func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) {
165	out := new(WithPointer)
166	type FooStruct struct{ A int }
167	in := map[string]interface{}{
168		"name": FooStruct{A: 5}, // This is the wrong type.
169	}
170	expectedErrorMessage := "jsonapi: Can't unmarshal map[A:5] (map) to struct field `Name`, which is a pointer to `string`"
171
172	err := UnmarshalPayload(sampleWithPointerPayload(in), out)
173
174	if err == nil {
175		t.Fatalf("Expected error due to invalid type.")
176	}
177	if err.Error() != expectedErrorMessage {
178		t.Fatalf("Unexpected error message: %s", err.Error())
179	}
180	if _, ok := err.(ErrUnsupportedPtrType); !ok {
181		t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
182	}
183}
184
185func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) {
186	out := new(WithPointer)
187	type FooStruct struct{ A, B int }
188	in := map[string]interface{}{
189		"name": []int{4, 5}, // This is the wrong type.
190	}
191	expectedErrorMessage := "jsonapi: Can't unmarshal [4 5] (slice) to struct field `Name`, which is a pointer to `string`"
192
193	err := UnmarshalPayload(sampleWithPointerPayload(in), out)
194
195	if err == nil {
196		t.Fatalf("Expected error due to invalid type.")
197	}
198	if err.Error() != expectedErrorMessage {
199		t.Fatalf("Unexpected error message: %s", err.Error())
200	}
201	if _, ok := err.(ErrUnsupportedPtrType); !ok {
202		t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
203	}
204}
205
206func TestStringPointerField(t *testing.T) {
207	// Build Book payload
208	description := "Hello World!"
209	data := map[string]interface{}{
210		"data": map[string]interface{}{
211			"type": "books",
212			"id":   "5",
213			"attributes": map[string]interface{}{
214				"author":      "aren55555",
215				"description": description,
216				"isbn":        "",
217			},
218		},
219	}
220	payload, err := json.Marshal(data)
221	if err != nil {
222		t.Fatal(err)
223	}
224
225	// Parse JSON API payload
226	book := new(Book)
227	if err := UnmarshalPayload(bytes.NewReader(payload), book); err != nil {
228		t.Fatal(err)
229	}
230
231	if book.Description == nil {
232		t.Fatal("Was not expecting a nil pointer for book.Description")
233	}
234	if expected, actual := description, *book.Description; expected != actual {
235		t.Fatalf("Was expecting descript to be `%s`, got `%s`", expected, actual)
236	}
237}
238
239func TestMalformedTag(t *testing.T) {
240	out := new(BadModel)
241	err := UnmarshalPayload(samplePayload(), out)
242	if err == nil || err != ErrBadJSONAPIStructTag {
243		t.Fatalf("Did not error out with wrong number of arguments in tag")
244	}
245}
246
247func TestUnmarshalInvalidJSON(t *testing.T) {
248	in := strings.NewReader("{}")
249	out := new(Blog)
250
251	err := UnmarshalPayload(in, out)
252
253	if err == nil {
254		t.Fatalf("Did not error out the invalid JSON.")
255	}
256}
257
258func TestUnmarshalInvalidJSON_BadType(t *testing.T) {
259	var badTypeTests = []struct {
260		Field    string
261		BadValue interface{}
262		Error    error
263	}{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields.
264		{Field: "string_field", BadValue: 0, Error: ErrUnknownFieldNumberType},  // Expected string.
265		{Field: "float_field", BadValue: "A string.", Error: ErrInvalidType},    // Expected float64.
266		{Field: "time_field", BadValue: "A string.", Error: ErrInvalidTime},     // Expected int64.
267		{Field: "time_ptr_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected *time / int64.
268	}
269	for _, test := range badTypeTests {
270		t.Run(fmt.Sprintf("Test_%s", test.Field), func(t *testing.T) {
271			out := new(ModelBadTypes)
272			in := map[string]interface{}{}
273			in[test.Field] = test.BadValue
274			expectedErrorMessage := test.Error.Error()
275
276			err := UnmarshalPayload(samplePayloadWithBadTypes(in), out)
277
278			if err == nil {
279				t.Fatalf("Expected error due to invalid type.")
280			}
281			if err.Error() != expectedErrorMessage {
282				t.Fatalf("Unexpected error message: %s", err.Error())
283			}
284		})
285	}
286}
287
288func TestUnmarshalSetsID(t *testing.T) {
289	in := samplePayloadWithID()
290	out := new(Blog)
291
292	if err := UnmarshalPayload(in, out); err != nil {
293		t.Fatal(err)
294	}
295
296	if out.ID != 2 {
297		t.Fatalf("Did not set ID on dst interface")
298	}
299}
300
301func TestUnmarshal_nonNumericID(t *testing.T) {
302	data := samplePayloadWithoutIncluded()
303	data["data"].(map[string]interface{})["id"] = "non-numeric-id"
304	payload, _ := payload(data)
305	in := bytes.NewReader(payload)
306	out := new(Post)
307
308	if err := UnmarshalPayload(in, out); err != ErrBadJSONAPIID {
309		t.Fatalf(
310			"Was expecting a `%s` error, got `%s`",
311			ErrBadJSONAPIID,
312			err,
313		)
314	}
315}
316
317func TestUnmarshalSetsAttrs(t *testing.T) {
318	out, err := unmarshalSamplePayload()
319	if err != nil {
320		t.Fatal(err)
321	}
322
323	if out.CreatedAt.IsZero() {
324		t.Fatalf("Did not parse time")
325	}
326
327	if out.ViewCount != 1000 {
328		t.Fatalf("View count not properly serialized")
329	}
330}
331
332func TestUnmarshalParsesISO8601(t *testing.T) {
333	payload := &OnePayload{
334		Data: &Node{
335			Type: "timestamps",
336			Attributes: map[string]interface{}{
337				"timestamp": "2016-08-17T08:27:12Z",
338			},
339		},
340	}
341
342	in := bytes.NewBuffer(nil)
343	json.NewEncoder(in).Encode(payload)
344
345	out := new(Timestamp)
346
347	if err := UnmarshalPayload(in, out); err != nil {
348		t.Fatal(err)
349	}
350
351	expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
352
353	if !out.Time.Equal(expected) {
354		t.Fatal("Parsing the ISO8601 timestamp failed")
355	}
356}
357
358func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
359	payload := &OnePayload{
360		Data: &Node{
361			Type: "timestamps",
362			Attributes: map[string]interface{}{
363				"next": "2016-08-17T08:27:12Z",
364			},
365		},
366	}
367
368	in := bytes.NewBuffer(nil)
369	json.NewEncoder(in).Encode(payload)
370
371	out := new(Timestamp)
372
373	if err := UnmarshalPayload(in, out); err != nil {
374		t.Fatal(err)
375	}
376
377	expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
378
379	if !out.Next.Equal(expected) {
380		t.Fatal("Parsing the ISO8601 timestamp failed")
381	}
382}
383
384func TestUnmarshalInvalidISO8601(t *testing.T) {
385	payload := &OnePayload{
386		Data: &Node{
387			Type: "timestamps",
388			Attributes: map[string]interface{}{
389				"timestamp": "17 Aug 16 08:027 MST",
390			},
391		},
392	}
393
394	in := bytes.NewBuffer(nil)
395	json.NewEncoder(in).Encode(payload)
396
397	out := new(Timestamp)
398
399	if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 {
400		t.Fatalf("Expected ErrInvalidISO8601, got %v", err)
401	}
402}
403
404func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
405	data, _ := payload(samplePayloadWithoutIncluded())
406	in := bytes.NewReader(data)
407	out := new(Post)
408
409	if err := UnmarshalPayload(in, out); err != nil {
410		t.Fatal(err)
411	}
412
413	// Verify each comment has at least an ID
414	for _, comment := range out.Comments {
415		if comment.ID == 0 {
416			t.Fatalf("The comment did not have an ID")
417		}
418	}
419}
420
421func TestUnmarshalRelationships(t *testing.T) {
422	out, err := unmarshalSamplePayload()
423	if err != nil {
424		t.Fatal(err)
425	}
426
427	if out.CurrentPost == nil {
428		t.Fatalf("Current post was not materialized")
429	}
430
431	if out.CurrentPost.Title != "Bas" || out.CurrentPost.Body != "Fuubar" {
432		t.Fatalf("Attributes were not set")
433	}
434
435	if len(out.Posts) != 2 {
436		t.Fatalf("There should have been 2 posts")
437	}
438}
439
440func TestUnmarshalNullRelationship(t *testing.T) {
441	sample := map[string]interface{}{
442		"data": map[string]interface{}{
443			"type": "posts",
444			"id":   "1",
445			"attributes": map[string]interface{}{
446				"body":  "Hello",
447				"title": "World",
448			},
449			"relationships": map[string]interface{}{
450				"latest_comment": map[string]interface{}{
451					"data": nil, // empty to-one relationship
452				},
453			},
454		},
455	}
456	data, err := json.Marshal(sample)
457	if err != nil {
458		t.Fatal(err)
459	}
460
461	in := bytes.NewReader(data)
462	out := new(Post)
463
464	if err := UnmarshalPayload(in, out); err != nil {
465		t.Fatal(err)
466	}
467
468	if out.LatestComment != nil {
469		t.Fatalf("Latest Comment was not set to nil")
470	}
471}
472
473func TestUnmarshalNullRelationshipInSlice(t *testing.T) {
474	sample := map[string]interface{}{
475		"data": map[string]interface{}{
476			"type": "posts",
477			"id":   "1",
478			"attributes": map[string]interface{}{
479				"body":  "Hello",
480				"title": "World",
481			},
482			"relationships": map[string]interface{}{
483				"comments": map[string]interface{}{
484					"data": []interface{}{}, // empty to-many relationships
485				},
486			},
487		},
488	}
489	data, err := json.Marshal(sample)
490	if err != nil {
491		t.Fatal(err)
492	}
493
494	in := bytes.NewReader(data)
495	out := new(Post)
496
497	if err := UnmarshalPayload(in, out); err != nil {
498		t.Fatal(err)
499	}
500
501	if len(out.Comments) != 0 {
502		t.Fatalf("Wrong number of comments; Comments should be empty")
503	}
504}
505
506func TestUnmarshalNestedRelationships(t *testing.T) {
507	out, err := unmarshalSamplePayload()
508	if err != nil {
509		t.Fatal(err)
510	}
511
512	if out.CurrentPost == nil {
513		t.Fatalf("Current post was not materialized")
514	}
515
516	if out.CurrentPost.Comments == nil {
517		t.Fatalf("Did not materialize nested records, comments")
518	}
519
520	if len(out.CurrentPost.Comments) != 2 {
521		t.Fatalf("Wrong number of comments")
522	}
523}
524
525func TestUnmarshalRelationshipsSerializedEmbedded(t *testing.T) {
526	out := sampleSerializedEmbeddedTestModel()
527
528	if out.CurrentPost == nil {
529		t.Fatalf("Current post was not materialized")
530	}
531
532	if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" {
533		t.Fatalf("Attributes were not set")
534	}
535
536	if len(out.Posts) != 2 {
537		t.Fatalf("There should have been 2 posts")
538	}
539
540	if out.Posts[0].LatestComment.Body != "foo" {
541		t.Fatalf("The comment body was not set")
542	}
543}
544
545func TestUnmarshalNestedRelationshipsEmbedded(t *testing.T) {
546	out := bytes.NewBuffer(nil)
547	if err := MarshalOnePayloadEmbedded(out, testModel()); err != nil {
548		t.Fatal(err)
549	}
550
551	model := new(Blog)
552
553	if err := UnmarshalPayload(out, model); err != nil {
554		t.Fatal(err)
555	}
556
557	if model.CurrentPost == nil {
558		t.Fatalf("Current post was not materialized")
559	}
560
561	if model.CurrentPost.Comments == nil {
562		t.Fatalf("Did not materialize nested records, comments")
563	}
564
565	if len(model.CurrentPost.Comments) != 2 {
566		t.Fatalf("Wrong number of comments")
567	}
568
569	if model.CurrentPost.Comments[0].Body != "foo" {
570		t.Fatalf("Comment body not set")
571	}
572}
573
574func TestUnmarshalRelationshipsSideloaded(t *testing.T) {
575	payload := samplePayloadWithSideloaded()
576	out := new(Blog)
577
578	if err := UnmarshalPayload(payload, out); err != nil {
579		t.Fatal(err)
580	}
581
582	if out.CurrentPost == nil {
583		t.Fatalf("Current post was not materialized")
584	}
585
586	if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" {
587		t.Fatalf("Attributes were not set")
588	}
589
590	if len(out.Posts) != 2 {
591		t.Fatalf("There should have been 2 posts")
592	}
593}
594
595func TestUnmarshalNestedRelationshipsSideloaded(t *testing.T) {
596	payload := samplePayloadWithSideloaded()
597	out := new(Blog)
598
599	if err := UnmarshalPayload(payload, out); err != nil {
600		t.Fatal(err)
601	}
602
603	if out.CurrentPost == nil {
604		t.Fatalf("Current post was not materialized")
605	}
606
607	if out.CurrentPost.Comments == nil {
608		t.Fatalf("Did not materialize nested records, comments")
609	}
610
611	if len(out.CurrentPost.Comments) != 2 {
612		t.Fatalf("Wrong number of comments")
613	}
614
615	if out.CurrentPost.Comments[0].Body != "foo" {
616		t.Fatalf("Comment body not set")
617	}
618}
619
620func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) {
621	model := new(Blog)
622
623	if err := UnmarshalPayload(samplePayload(), model); err != nil {
624		t.Fatal(err)
625	}
626
627	if model.Posts[0].ClientID == "" {
628		t.Fatalf("ClientID not set from request on related record")
629	}
630}
631
632func unmarshalSamplePayload() (*Blog, error) {
633	in := samplePayload()
634	out := new(Blog)
635
636	if err := UnmarshalPayload(in, out); err != nil {
637		return nil, err
638	}
639
640	return out, nil
641}
642
643func TestUnmarshalManyPayload(t *testing.T) {
644	sample := map[string]interface{}{
645		"data": []interface{}{
646			map[string]interface{}{
647				"type": "posts",
648				"id":   "1",
649				"attributes": map[string]interface{}{
650					"body":  "First",
651					"title": "Post",
652				},
653			},
654			map[string]interface{}{
655				"type": "posts",
656				"id":   "2",
657				"attributes": map[string]interface{}{
658					"body":  "Second",
659					"title": "Post",
660				},
661			},
662		},
663	}
664
665	data, err := json.Marshal(sample)
666	if err != nil {
667		t.Fatal(err)
668	}
669	in := bytes.NewReader(data)
670
671	posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post)))
672	if err != nil {
673		t.Fatal(err)
674	}
675
676	if len(posts) != 2 {
677		t.Fatal("Wrong number of posts")
678	}
679
680	for _, p := range posts {
681		_, ok := p.(*Post)
682		if !ok {
683			t.Fatal("Was expecting a Post")
684		}
685	}
686}
687
688func TestManyPayload_withLinks(t *testing.T) {
689	firstPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=50"
690	prevPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=0"
691	nextPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=100"
692	lastPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=500"
693
694	sample := map[string]interface{}{
695		"data": []interface{}{
696			map[string]interface{}{
697				"type": "posts",
698				"id":   "1",
699				"attributes": map[string]interface{}{
700					"body":  "First",
701					"title": "Post",
702				},
703			},
704			map[string]interface{}{
705				"type": "posts",
706				"id":   "2",
707				"attributes": map[string]interface{}{
708					"body":  "Second",
709					"title": "Post",
710				},
711			},
712		},
713		"links": map[string]interface{}{
714			KeyFirstPage:    firstPageURL,
715			KeyPreviousPage: prevPageURL,
716			KeyNextPage:     nextPageURL,
717			KeyLastPage:     lastPageURL,
718		},
719	}
720
721	data, err := json.Marshal(sample)
722	if err != nil {
723		t.Fatal(err)
724	}
725	in := bytes.NewReader(data)
726
727	payload := new(ManyPayload)
728	if err = json.NewDecoder(in).Decode(payload); err != nil {
729		t.Fatal(err)
730	}
731
732	if payload.Links == nil {
733		t.Fatal("Was expecting a non nil ptr Link field")
734	}
735
736	links := *payload.Links
737
738	first, ok := links[KeyFirstPage]
739	if !ok {
740		t.Fatal("Was expecting a non nil ptr Link field")
741	}
742	if e, a := firstPageURL, first; e != a {
743		t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyFirstPage, e, a)
744	}
745
746	prev, ok := links[KeyPreviousPage]
747	if !ok {
748		t.Fatal("Was expecting a non nil ptr Link field")
749	}
750	if e, a := prevPageURL, prev; e != a {
751		t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyPreviousPage, e, a)
752	}
753
754	next, ok := links[KeyNextPage]
755	if !ok {
756		t.Fatal("Was expecting a non nil ptr Link field")
757	}
758	if e, a := nextPageURL, next; e != a {
759		t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyNextPage, e, a)
760	}
761
762	last, ok := links[KeyLastPage]
763	if !ok {
764		t.Fatal("Was expecting a non nil ptr Link field")
765	}
766	if e, a := lastPageURL, last; e != a {
767		t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyLastPage, e, a)
768	}
769}
770
771func TestUnmarshalCustomTypeAttributes(t *testing.T) {
772	customInt := CustomIntType(5)
773	customFloat := CustomFloatType(1.5)
774	customString := CustomStringType("Test")
775
776	data := map[string]interface{}{
777		"data": map[string]interface{}{
778			"type": "customtypes",
779			"id":   "1",
780			"attributes": map[string]interface{}{
781				"int":        customInt,
782				"intptr":     &customInt,
783				"intptrnull": nil,
784
785				"float":  customFloat,
786				"string": customString,
787			},
788		},
789	}
790	payload, err := payload(data)
791	if err != nil {
792		t.Fatal(err)
793	}
794
795	// Parse JSON API payload
796	customAttributeTypes := new(CustomAttributeTypes)
797	if err := UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes); err != nil {
798		t.Fatal(err)
799	}
800
801	if expected, actual := customInt, customAttributeTypes.Int; expected != actual {
802		t.Fatalf("Was expecting custom int to be `%d`, got `%d`", expected, actual)
803	}
804	if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual {
805		t.Fatalf("Was expecting custom int pointer to be `%d`, got `%d`", expected, actual)
806	}
807	if customAttributeTypes.IntPtrNull != nil {
808		t.Fatalf("Was expecting custom int pointer to be <nil>, got `%d`", customAttributeTypes.IntPtrNull)
809	}
810
811	if expected, actual := customFloat, customAttributeTypes.Float; expected != actual {
812		t.Fatalf("Was expecting custom float to be `%f`, got `%f`", expected, actual)
813	}
814	if expected, actual := customString, customAttributeTypes.String; expected != actual {
815		t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual)
816	}
817}
818
819func samplePayloadWithoutIncluded() map[string]interface{} {
820	return map[string]interface{}{
821		"data": map[string]interface{}{
822			"type": "posts",
823			"id":   "1",
824			"attributes": map[string]interface{}{
825				"body":  "Hello",
826				"title": "World",
827			},
828			"relationships": map[string]interface{}{
829				"comments": map[string]interface{}{
830					"data": []interface{}{
831						map[string]interface{}{
832							"type": "comments",
833							"id":   "123",
834						},
835						map[string]interface{}{
836							"type": "comments",
837							"id":   "456",
838						},
839					},
840				},
841				"latest_comment": map[string]interface{}{
842					"data": map[string]interface{}{
843						"type": "comments",
844						"id":   "55555",
845					},
846				},
847			},
848		},
849	}
850}
851
852func payload(data map[string]interface{}) (result []byte, err error) {
853	result, err = json.Marshal(data)
854	return
855}
856
857func samplePayload() io.Reader {
858	payload := &OnePayload{
859		Data: &Node{
860			Type: "blogs",
861			Attributes: map[string]interface{}{
862				"title":      "New blog",
863				"created_at": 1436216820,
864				"view_count": 1000,
865			},
866			Relationships: map[string]interface{}{
867				"posts": &RelationshipManyNode{
868					Data: []*Node{
869						{
870							Type: "posts",
871							Attributes: map[string]interface{}{
872								"title": "Foo",
873								"body":  "Bar",
874							},
875							ClientID: "1",
876						},
877						{
878							Type: "posts",
879							Attributes: map[string]interface{}{
880								"title": "X",
881								"body":  "Y",
882							},
883							ClientID: "2",
884						},
885					},
886				},
887				"current_post": &RelationshipOneNode{
888					Data: &Node{
889						Type: "posts",
890						Attributes: map[string]interface{}{
891							"title": "Bas",
892							"body":  "Fuubar",
893						},
894						ClientID: "3",
895						Relationships: map[string]interface{}{
896							"comments": &RelationshipManyNode{
897								Data: []*Node{
898									{
899										Type: "comments",
900										Attributes: map[string]interface{}{
901											"body": "Great post!",
902										},
903										ClientID: "4",
904									},
905									{
906										Type: "comments",
907										Attributes: map[string]interface{}{
908											"body": "Needs some work!",
909										},
910										ClientID: "5",
911									},
912								},
913							},
914						},
915					},
916				},
917			},
918		},
919	}
920
921	out := bytes.NewBuffer(nil)
922	json.NewEncoder(out).Encode(payload)
923
924	return out
925}
926
927func samplePayloadWithID() io.Reader {
928	payload := &OnePayload{
929		Data: &Node{
930			ID:   "2",
931			Type: "blogs",
932			Attributes: map[string]interface{}{
933				"title":      "New blog",
934				"view_count": 1000,
935			},
936		},
937	}
938
939	out := bytes.NewBuffer(nil)
940	json.NewEncoder(out).Encode(payload)
941
942	return out
943}
944
945func samplePayloadWithBadTypes(m map[string]interface{}) io.Reader {
946	payload := &OnePayload{
947		Data: &Node{
948			ID:         "2",
949			Type:       "badtypes",
950			Attributes: m,
951		},
952	}
953
954	out := bytes.NewBuffer(nil)
955	json.NewEncoder(out).Encode(payload)
956
957	return out
958}
959
960func sampleWithPointerPayload(m map[string]interface{}) io.Reader {
961	payload := &OnePayload{
962		Data: &Node{
963			ID:         "2",
964			Type:       "with-pointers",
965			Attributes: m,
966		},
967	}
968
969	out := bytes.NewBuffer(nil)
970	json.NewEncoder(out).Encode(payload)
971
972	return out
973}
974
975func testModel() *Blog {
976	return &Blog{
977		ID:        5,
978		ClientID:  "1",
979		Title:     "Title 1",
980		CreatedAt: time.Now(),
981		Posts: []*Post{
982			{
983				ID:    1,
984				Title: "Foo",
985				Body:  "Bar",
986				Comments: []*Comment{
987					{
988						ID:   1,
989						Body: "foo",
990					},
991					{
992						ID:   2,
993						Body: "bar",
994					},
995				},
996				LatestComment: &Comment{
997					ID:   1,
998					Body: "foo",
999				},
1000			},
1001			{
1002				ID:    2,
1003				Title: "Fuubar",
1004				Body:  "Bas",
1005				Comments: []*Comment{
1006					{
1007						ID:   1,
1008						Body: "foo",
1009					},
1010					{
1011						ID:   3,
1012						Body: "bas",
1013					},
1014				},
1015				LatestComment: &Comment{
1016					ID:   1,
1017					Body: "foo",
1018				},
1019			},
1020		},
1021		CurrentPost: &Post{
1022			ID:    1,
1023			Title: "Foo",
1024			Body:  "Bar",
1025			Comments: []*Comment{
1026				{
1027					ID:   1,
1028					Body: "foo",
1029				},
1030				{
1031					ID:   2,
1032					Body: "bar",
1033				},
1034			},
1035			LatestComment: &Comment{
1036				ID:   1,
1037				Body: "foo",
1038			},
1039		},
1040	}
1041}
1042
1043func samplePayloadWithSideloaded() io.Reader {
1044	testModel := testModel()
1045
1046	out := bytes.NewBuffer(nil)
1047	MarshalPayload(out, testModel)
1048
1049	return out
1050}
1051
1052func sampleSerializedEmbeddedTestModel() *Blog {
1053	out := bytes.NewBuffer(nil)
1054	MarshalOnePayloadEmbedded(out, testModel())
1055
1056	blog := new(Blog)
1057	UnmarshalPayload(out, blog)
1058
1059	return blog
1060}
1061
1062func TestUnmarshalNestedStructPtr(t *testing.T) {
1063	type Director struct {
1064		Firstname string `json:"firstname"`
1065		Surname   string `json:"surname"`
1066	}
1067	type Movie struct {
1068		ID       string    `jsonapi:"primary,movies"`
1069		Name     string    `jsonapi:"attr,name"`
1070		Director *Director `jsonapi:"attr,director"`
1071	}
1072	sample := map[string]interface{}{
1073		"data": map[string]interface{}{
1074			"type": "movies",
1075			"id":   "123",
1076			"attributes": map[string]interface{}{
1077				"name": "The Shawshank Redemption",
1078				"director": map[string]interface{}{
1079					"firstname": "Frank",
1080					"surname":   "Darabont",
1081				},
1082			},
1083		},
1084	}
1085
1086	data, err := json.Marshal(sample)
1087	if err != nil {
1088		t.Fatal(err)
1089	}
1090	in := bytes.NewReader(data)
1091	out := new(Movie)
1092
1093	if err := UnmarshalPayload(in, out); err != nil {
1094		t.Fatal(err)
1095	}
1096
1097	if out.Name != "The Shawshank Redemption" {
1098		t.Fatalf("expected out.Name to be `The Shawshank Redemption`, but got `%s`", out.Name)
1099	}
1100	if out.Director.Firstname != "Frank" {
1101		t.Fatalf("expected out.Director.Firstname to be `Frank`, but got `%s`", out.Director.Firstname)
1102	}
1103	if out.Director.Surname != "Darabont" {
1104		t.Fatalf("expected out.Director.Surname to be `Darabont`, but got `%s`", out.Director.Surname)
1105	}
1106}
1107
1108func TestUnmarshalNestedStruct(t *testing.T) {
1109
1110	boss := map[string]interface{}{
1111		"firstname": "Hubert",
1112		"surname":   "Farnsworth",
1113		"age":       176,
1114		"hired-at":  "2016-08-17T08:27:12Z",
1115	}
1116
1117	sample := map[string]interface{}{
1118		"data": map[string]interface{}{
1119			"type": "companies",
1120			"id":   "123",
1121			"attributes": map[string]interface{}{
1122				"name":       "Planet Express",
1123				"boss":       boss,
1124				"founded-at": "2016-08-17T08:27:12Z",
1125				"teams": []Team{
1126					Team{
1127						Name: "Dev",
1128						Members: []Employee{
1129							Employee{Firstname: "Sean"},
1130							Employee{Firstname: "Iz"},
1131						},
1132						Leader: &Employee{Firstname: "Iz"},
1133					},
1134					Team{
1135						Name: "DxE",
1136						Members: []Employee{
1137							Employee{Firstname: "Akshay"},
1138							Employee{Firstname: "Peri"},
1139						},
1140						Leader: &Employee{Firstname: "Peri"},
1141					},
1142				},
1143			},
1144		},
1145	}
1146
1147	data, err := json.Marshal(sample)
1148	if err != nil {
1149		t.Fatal(err)
1150	}
1151	in := bytes.NewReader(data)
1152	out := new(Company)
1153
1154	if err := UnmarshalPayload(in, out); err != nil {
1155		t.Fatal(err)
1156	}
1157
1158	if out.Boss.Firstname != "Hubert" {
1159		t.Fatalf("expected `Hubert` at out.Boss.Firstname, but got `%s`", out.Boss.Firstname)
1160	}
1161
1162	if out.Boss.Age != 176 {
1163		t.Fatalf("expected `176` at out.Boss.Age, but got `%d`", out.Boss.Age)
1164	}
1165
1166	if out.Boss.HiredAt.IsZero() {
1167		t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%t`", out.Boss.HiredAt.IsZero())
1168	}
1169
1170	if len(out.Teams) != 2 {
1171		t.Fatalf("expected len(out.Teams) to be 2, but got `%d`", len(out.Teams))
1172	}
1173
1174	if out.Teams[0].Name != "Dev" {
1175		t.Fatalf("expected out.Teams[0].Name to be `Dev`, but got `%s`", out.Teams[0].Name)
1176	}
1177
1178	if out.Teams[1].Name != "DxE" {
1179		t.Fatalf("expected out.Teams[1].Name to be `DxE`, but got `%s`", out.Teams[1].Name)
1180	}
1181
1182	if len(out.Teams[0].Members) != 2 {
1183		t.Fatalf("expected len(out.Teams[0].Members) to be 2, but got `%d`", len(out.Teams[0].Members))
1184	}
1185
1186	if len(out.Teams[1].Members) != 2 {
1187		t.Fatalf("expected len(out.Teams[1].Members) to be 2, but got `%d`", len(out.Teams[1].Members))
1188	}
1189
1190	if out.Teams[0].Members[0].Firstname != "Sean" {
1191		t.Fatalf("expected out.Teams[0].Members[0].Firstname to be `Sean`, but got `%s`", out.Teams[0].Members[0].Firstname)
1192	}
1193
1194	if out.Teams[0].Members[1].Firstname != "Iz" {
1195		t.Fatalf("expected out.Teams[0].Members[1].Firstname to be `Iz`, but got `%s`", out.Teams[0].Members[1].Firstname)
1196	}
1197
1198	if out.Teams[1].Members[0].Firstname != "Akshay" {
1199		t.Fatalf("expected out.Teams[1].Members[0].Firstname to be `Akshay`, but got `%s`", out.Teams[1].Members[0].Firstname)
1200	}
1201
1202	if out.Teams[1].Members[1].Firstname != "Peri" {
1203		t.Fatalf("expected out.Teams[1].Members[1].Firstname to be `Peri`, but got `%s`", out.Teams[1].Members[1].Firstname)
1204	}
1205
1206	if out.Teams[0].Leader.Firstname != "Iz" {
1207		t.Fatalf("expected out.Teams[0].Leader.Firstname to be `Iz`, but got `%s`", out.Teams[0].Leader.Firstname)
1208	}
1209
1210	if out.Teams[1].Leader.Firstname != "Peri" {
1211		t.Fatalf("expected out.Teams[1].Leader.Firstname to be `Peri`, but got `%s`", out.Teams[1].Leader.Firstname)
1212	}
1213}
1214
1215func TestUnmarshalNestedStructSlice(t *testing.T) {
1216
1217	fry := map[string]interface{}{
1218		"firstname": "Philip J.",
1219		"surname":   "Fry",
1220		"age":       25,
1221		"hired-at":  "2016-08-17T08:27:12Z",
1222	}
1223
1224	bender := map[string]interface{}{
1225		"firstname": "Bender Bending",
1226		"surname":   "Rodriguez",
1227		"age":       19,
1228		"hired-at":  "2016-08-17T08:27:12Z",
1229	}
1230
1231	deliveryCrew := map[string]interface{}{
1232		"name": "Delivery Crew",
1233		"members": []interface{}{
1234			fry,
1235			bender,
1236		},
1237	}
1238
1239	sample := map[string]interface{}{
1240		"data": map[string]interface{}{
1241			"type": "companies",
1242			"id":   "123",
1243			"attributes": map[string]interface{}{
1244				"name": "Planet Express",
1245				"teams": []interface{}{
1246					deliveryCrew,
1247				},
1248			},
1249		},
1250	}
1251
1252	data, err := json.Marshal(sample)
1253	if err != nil {
1254		t.Fatal(err)
1255	}
1256	in := bytes.NewReader(data)
1257	out := new(Company)
1258
1259	if err := UnmarshalPayload(in, out); err != nil {
1260		t.Fatal(err)
1261	}
1262
1263	if out.Teams[0].Name != "Delivery Crew" {
1264		t.Fatalf("Nested struct not unmarshalled: Expected `Delivery Crew` but got `%s`", out.Teams[0].Name)
1265	}
1266
1267	if len(out.Teams[0].Members) != 2 {
1268		t.Fatalf("Nested struct not unmarshalled: Expected to have `2` Members but got `%d`",
1269			len(out.Teams[0].Members))
1270	}
1271
1272	if out.Teams[0].Members[0].Firstname != "Philip J." {
1273		t.Fatalf("Nested struct not unmarshalled: Expected `Philip J.` but got `%s`",
1274			out.Teams[0].Members[0].Firstname)
1275	}
1276}
1277