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