1// Copyright 2016 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package datastore
16
17import (
18	"fmt"
19	"testing"
20	"time"
21
22	"cloud.google.com/go/civil"
23	"cloud.google.com/go/internal/testutil"
24	pb "google.golang.org/genproto/googleapis/datastore/v1"
25)
26
27func TestInterfaceToProtoNil(t *testing.T) {
28	// A nil *Key, or a nil value of any other pointer type, should convert to a NullValue.
29	for _, in := range []interface{}{
30		(*Key)(nil),
31		(*int)(nil),
32		(*string)(nil),
33		(*bool)(nil),
34		(*float64)(nil),
35		(*GeoPoint)(nil),
36		(*time.Time)(nil),
37	} {
38		got, err := interfaceToProto(in, false)
39		if err != nil {
40			t.Fatalf("%T: %v", in, err)
41		}
42		_, ok := got.ValueType.(*pb.Value_NullValue)
43		if !ok {
44			t.Errorf("%T: got: %T\nwant: %T", in, got.ValueType, &pb.Value_NullValue{})
45		}
46	}
47}
48
49func TestSaveEntityNested(t *testing.T) {
50	type WithKey struct {
51		X string
52		I int
53		K *Key `datastore:"__key__"`
54	}
55
56	type NestedWithKey struct {
57		Y string
58		N WithKey
59	}
60
61	type WithoutKey struct {
62		X string
63		I int
64	}
65
66	type NestedWithoutKey struct {
67		Y string
68		N WithoutKey
69	}
70
71	type a struct {
72		S string
73	}
74
75	type UnexpAnonym struct {
76		a
77	}
78
79	testCases := []struct {
80		desc string
81		src  interface{}
82		key  *Key
83		want *pb.Entity
84	}{
85		{
86			desc: "nested entity with key",
87			src: &NestedWithKey{
88				Y: "yyy",
89				N: WithKey{
90					X: "two",
91					I: 2,
92					K: testKey1a,
93				},
94			},
95			key: testKey0,
96			want: &pb.Entity{
97				Key: keyToProto(testKey0),
98				Properties: map[string]*pb.Value{
99					"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
100					"N": {ValueType: &pb.Value_EntityValue{
101						EntityValue: &pb.Entity{
102							Key: keyToProto(testKey1a),
103							Properties: map[string]*pb.Value{
104								"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
105								"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
106							},
107						},
108					}},
109				},
110			},
111		},
112		{
113			desc: "nested entity with incomplete key",
114			src: &NestedWithKey{
115				Y: "yyy",
116				N: WithKey{
117					X: "two",
118					I: 2,
119					K: incompleteKey,
120				},
121			},
122			key: testKey0,
123			want: &pb.Entity{
124				Key: keyToProto(testKey0),
125				Properties: map[string]*pb.Value{
126					"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
127					"N": {ValueType: &pb.Value_EntityValue{
128						EntityValue: &pb.Entity{
129							Key: keyToProto(incompleteKey),
130							Properties: map[string]*pb.Value{
131								"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
132								"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
133							},
134						},
135					}},
136				},
137			},
138		},
139		{
140			desc: "nested entity without key",
141			src: &NestedWithoutKey{
142				Y: "yyy",
143				N: WithoutKey{
144					X: "two",
145					I: 2,
146				},
147			},
148			key: testKey0,
149			want: &pb.Entity{
150				Key: keyToProto(testKey0),
151				Properties: map[string]*pb.Value{
152					"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
153					"N": {ValueType: &pb.Value_EntityValue{
154						EntityValue: &pb.Entity{
155							Properties: map[string]*pb.Value{
156								"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
157								"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
158							},
159						},
160					}},
161				},
162			},
163		},
164		{
165			desc: "key at top level",
166			src: &WithKey{
167				X: "three",
168				I: 3,
169				K: testKey0,
170			},
171			key: testKey0,
172			want: &pb.Entity{
173				Key: keyToProto(testKey0),
174				Properties: map[string]*pb.Value{
175					"X": {ValueType: &pb.Value_StringValue{StringValue: "three"}},
176					"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
177				},
178			},
179		},
180		{
181			desc: "nested unexported anonymous struct field",
182			src: &UnexpAnonym{
183				a{S: "hello"},
184			},
185			key: testKey0,
186			want: &pb.Entity{
187				Key: keyToProto(testKey0),
188				Properties: map[string]*pb.Value{
189					"S": {ValueType: &pb.Value_StringValue{StringValue: "hello"}},
190				},
191			},
192		},
193	}
194
195	for _, tc := range testCases {
196		got, err := saveEntity(tc.key, tc.src)
197		if err != nil {
198			t.Errorf("saveEntity: %s: %v", tc.desc, err)
199			continue
200		}
201
202		if !testutil.Equal(tc.want, got) {
203			t.Errorf("%s: compare:\ngot:  %#v\nwant: %#v", tc.desc, got, tc.want)
204		}
205	}
206}
207
208func TestSavePointers(t *testing.T) {
209	for _, test := range []struct {
210		desc string
211		in   interface{}
212		want []Property
213	}{
214		{
215			desc: "nil pointers save as nil-valued properties",
216			in:   &Pointers{},
217			want: []Property{
218				{Name: "Pi", Value: nil},
219				{Name: "Ps", Value: nil},
220				{Name: "Pb", Value: nil},
221				{Name: "Pf", Value: nil},
222				{Name: "Pg", Value: nil},
223				{Name: "Pt", Value: nil},
224			},
225		},
226		{
227			desc: "nil omitempty pointers not saved",
228			in:   &PointersOmitEmpty{},
229			want: []Property(nil),
230		},
231		{
232			desc: "non-nil omitempty zero-valued pointers are saved",
233			in:   func() *PointersOmitEmpty { pi := 0; return &PointersOmitEmpty{Pi: &pi} }(),
234			want: []Property{{Name: "Pi", Value: int64(0)}},
235		},
236		{
237			desc: "non-nil zero-valued pointers save as zero values",
238			in:   populatedPointers(),
239			want: []Property{
240				{Name: "Pi", Value: int64(0)},
241				{Name: "Ps", Value: ""},
242				{Name: "Pb", Value: false},
243				{Name: "Pf", Value: 0.0},
244				{Name: "Pg", Value: GeoPoint{}},
245				{Name: "Pt", Value: time.Time{}},
246			},
247		},
248		{
249			desc: "non-nil non-zero-valued pointers save as the appropriate values",
250			in: func() *Pointers {
251				p := populatedPointers()
252				*p.Pi = 1
253				*p.Ps = "x"
254				*p.Pb = true
255				*p.Pf = 3.14
256				*p.Pg = GeoPoint{Lat: 1, Lng: 2}
257				*p.Pt = time.Unix(100, 0)
258				return p
259			}(),
260			want: []Property{
261				{Name: "Pi", Value: int64(1)},
262				{Name: "Ps", Value: "x"},
263				{Name: "Pb", Value: true},
264				{Name: "Pf", Value: 3.14},
265				{Name: "Pg", Value: GeoPoint{Lat: 1, Lng: 2}},
266				{Name: "Pt", Value: time.Unix(100, 0)},
267			},
268		},
269	} {
270		got, err := SaveStruct(test.in)
271		if err != nil {
272			t.Fatalf("%s: %v", test.desc, err)
273		}
274		if !testutil.Equal(got, test.want) {
275			t.Errorf("%s\ngot  %#v\nwant %#v\n", test.desc, got, test.want)
276		}
277	}
278}
279
280func TestSaveEmptySlice(t *testing.T) {
281	// Zero-length slice fields are not saved.
282	for _, slice := range [][]string{nil, {}} {
283		got, err := SaveStruct(&struct{ S []string }{S: slice})
284		if err != nil {
285			t.Fatal(err)
286		}
287		if len(got) != 0 {
288			t.Errorf("%#v: got %d properties, wanted zero", slice, len(got))
289		}
290	}
291}
292
293// Map is used by TestSaveFieldsWithInterface
294// to test a custom type property save.
295type Map map[int]int
296
297func (*Map) Load(_ []Property) error {
298	return nil
299}
300
301func (*Map) Save() ([]Property, error) {
302	return []Property{}, nil
303}
304
305// Struct is used by TestSaveFieldsWithInterface
306// to test a custom type property save.
307type Struct struct {
308	Map Map
309}
310
311func TestSaveFieldsWithInterface(t *testing.T) {
312	// We should be able to extract the underlying value behind an interface.
313	// See issue https://github.com/googleapis/google-cloud-go/issues/1474.
314
315	type n1 struct {
316		Inner interface{}
317	}
318
319	type n2 struct {
320		Inner2 *n1
321	}
322	type n3 struct {
323		N2 interface{}
324	}
325
326	civDateVal := civil.Date{
327		Year:  2020,
328		Month: 11,
329		Day:   10,
330	}
331	civTimeValNano := civil.Time{
332		Hour:       1,
333		Minute:     1,
334		Second:     1,
335		Nanosecond: 1,
336	}
337	civTimeVal := civil.Time{
338		Hour:   1,
339		Minute: 1,
340		Second: 1,
341	}
342	timeValNano, _ := time.Parse("15:04:05.000000000", civTimeValNano.String())
343	timeVal, _ := time.Parse("15:04:05", civTimeVal.String())
344	dateTimeStr := fmt.Sprintf("%v %v", civDateVal.String(), civTimeVal.String())
345	dateTimeVal, _ := time.ParseInLocation("2006-01-02 15:04:05", dateTimeStr, time.UTC)
346
347	cases := []struct {
348		name string
349		in   interface{}
350		want []Property
351	}{
352		{
353			name: "Non-Nil value",
354			in: &struct {
355				Value interface{}
356				ID    int
357				key   interface{}
358			}{
359				Value: "this is a string",
360				ID:    17,
361				key:   "key1",
362			},
363			want: []Property{
364				{Name: "Value", Value: "this is a string"},
365				{Name: "ID", Value: int64(17)},
366			},
367		},
368		{
369			name: "Nil value",
370			in: &struct {
371				foo interface{}
372			}{
373				foo: (*string)(nil),
374			},
375			want: nil,
376		},
377		{
378			name: "Nil interface",
379			in: &struct {
380				Value interface{}
381				key   interface{}
382			}{
383				Value: nil,
384				key:   "key1",
385			},
386			want: []Property{
387				{Name: "Value", Value: nil},
388			},
389		},
390		{
391			name: "Nil map",
392			in:   &Struct{},
393			want: []Property{
394				{Name: "Map", Value: []Property{}},
395			},
396		},
397		{
398			name: "civil.Date",
399			in: &struct {
400				CivDate civil.Date
401			}{
402				CivDate: civDateVal,
403			},
404			want: []Property{
405				{
406					Name:  "CivDate",
407					Value: civDateVal.In(time.UTC),
408				},
409			},
410		},
411		{
412			name: "civil.Time-nano",
413			in: &struct {
414				CivTimeNano civil.Time
415			}{
416				CivTimeNano: civTimeValNano,
417			},
418			want: []Property{
419				{
420					Name:  "CivTimeNano",
421					Value: timeValNano,
422				},
423			},
424		},
425		{
426			name: "civil.Time",
427			in: &struct {
428				CivTime civil.Time
429			}{
430				CivTime: civTimeVal,
431			},
432			want: []Property{
433				{
434					Name:  "CivTime",
435					Value: timeVal,
436				},
437			},
438		},
439		{
440			name: "civil.DateTime",
441			in: &struct {
442				CivDateTime civil.DateTime
443			}{
444				CivDateTime: civil.DateTime{
445					Date: civDateVal,
446					Time: civTimeVal,
447				},
448			},
449			want: []Property{
450				{
451					Name:  "CivDateTime",
452					Value: dateTimeVal,
453				},
454			},
455		},
456		{
457			name: "Nested",
458			in: &n3{
459				N2: &n2{
460					Inner2: &n1{
461						Inner: "Innest",
462					},
463				},
464			},
465			want: []Property{
466				{
467					Name: "N2",
468					Value: &Entity{
469						Properties: []Property{{
470							Name: "Inner2",
471							Value: &Entity{
472								Properties: []Property{{
473									Name: "Inner", Value: "Innest",
474								}},
475							},
476						}},
477					},
478				},
479			},
480		},
481	}
482
483	for _, tt := range cases {
484		t.Run(tt.name, func(t *testing.T) {
485			got, err := SaveStruct(tt.in)
486			if err != nil {
487				t.Fatal(err)
488			}
489			if diff := testutil.Diff(got, tt.want); diff != "" {
490				t.Fatalf("got - want +\n%s", diff)
491			}
492		})
493	}
494}
495