// Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package firestore import ( "fmt" "reflect" "testing" "time" ts "github.com/golang/protobuf/ptypes/timestamp" pb "google.golang.org/genproto/googleapis/firestore/v1" "google.golang.org/genproto/googleapis/type/latlng" ) type testStruct1 struct { B bool I int U uint32 F float64 S string Y []byte T time.Time Ts *ts.Timestamp G *latlng.LatLng L []int M map[string]int P *int } var ( p = new(int) testVal1 = testStruct1{ B: true, I: 1, U: 2, F: 3.0, S: "four", Y: []byte{5}, T: tm, Ts: ptm, G: ll, L: []int{6}, M: map[string]int{"a": 7}, P: p, } mapVal1 = mapval(map[string]*pb.Value{ "B": boolval(true), "I": intval(1), "U": intval(2), "F": floatval(3), "S": {ValueType: &pb.Value_StringValue{"four"}}, "Y": bytesval([]byte{5}), "T": tsval(tm), "Ts": {ValueType: &pb.Value_TimestampValue{ptm}}, "G": geoval(ll), "L": arrayval(intval(6)), "M": mapval(map[string]*pb.Value{"a": intval(7)}), "P": intval(8), }) ) // TODO descriptions // TODO cause the array failure func TestToProtoValue_Conversions(t *testing.T) { *p = 8 for _, test := range []struct { desc string in interface{} want *pb.Value }{ { desc: "nil", in: nil, want: nullValue, }, { desc: "nil slice", in: []int(nil), want: nullValue, }, { desc: "nil map", in: map[string]int(nil), want: nullValue, }, { desc: "nil struct", in: (*testStruct1)(nil), want: nullValue, }, { desc: "nil timestamp", in: (*ts.Timestamp)(nil), want: nullValue, }, { desc: "nil latlng", in: (*latlng.LatLng)(nil), want: nullValue, }, { desc: "nil docref", in: (*DocumentRef)(nil), want: nullValue, }, { desc: "bool", in: true, want: boolval(true), }, { desc: "int", in: 3, want: intval(3), }, { desc: "uint32", in: uint32(3), want: intval(3), }, { desc: "float", in: 1.5, want: floatval(1.5), }, { desc: "string", in: "str", want: strval("str"), }, { desc: "byte slice", in: []byte{1, 2}, want: bytesval([]byte{1, 2}), }, { desc: "date time", in: tm, want: tsval(tm), }, { desc: "pointer to timestamp", in: ptm, want: &pb.Value{ValueType: &pb.Value_TimestampValue{ptm}}, }, { desc: "pointer to latlng", in: ll, want: geoval(ll), }, { desc: "populated slice", in: []int{1, 2}, want: arrayval(intval(1), intval(2)), }, { desc: "pointer to populated slice", in: &[]int{1, 2}, want: arrayval(intval(1), intval(2)), }, { desc: "empty slice", in: []int{}, want: arrayval(), }, { desc: "populated map", in: map[string]int{"a": 1, "b": 2}, want: mapval(map[string]*pb.Value{"a": intval(1), "b": intval(2)}), }, { desc: "empty map", in: map[string]int{}, want: mapval(map[string]*pb.Value{}), }, { desc: "int", in: p, want: intval(8), }, { desc: "pointer to int", in: &p, want: intval(8), }, { desc: "populated map", in: map[string]interface{}{"a": 1, "p": p, "s": "str"}, want: mapval(map[string]*pb.Value{"a": intval(1), "p": intval(8), "s": strval("str")}), }, { desc: "map with timestamp", in: map[string]fmt.Stringer{"a": tm}, want: mapval(map[string]*pb.Value{"a": tsval(tm)}), }, { desc: "struct", in: testVal1, want: mapVal1, }, { desc: "array", in: [1]int{7}, want: arrayval(intval(7)), }, { desc: "pointer to docref", in: &DocumentRef{ ID: "d", Path: "projects/P/databases/D/documents/c/d", Parent: &CollectionRef{ ID: "c", parentPath: "projects/P/databases/D", Path: "projects/P/databases/D/documents/c", Query: Query{collectionID: "c", parentPath: "projects/P/databases/D"}, }, }, want: refval("projects/P/databases/D/documents/c/d"), }, { desc: "Transforms are removed, which can lead to leaving nil", in: map[string]interface{}{"a": ServerTimestamp}, want: nil, }, { desc: "Transform nested in map is ignored", in: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": ServerTimestamp, }, }, }, want: nil, }, { desc: "Transforms nested in map are ignored", in: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": ServerTimestamp, "d": ServerTimestamp, }, }, }, want: nil, }, { desc: "int nested in map is kept whilst Transforms are ignored", in: map[string]interface{}{ "a": map[string]interface{}{ "b": map[string]interface{}{ "c": ServerTimestamp, "d": ServerTimestamp, "e": 1, }, }, }, want: mapval(map[string]*pb.Value{ "a": mapval(map[string]*pb.Value{ "b": mapval(map[string]*pb.Value{"e": intval(1)}), }), }), }, // Transforms are allowed in maps, but won't show up in the returned proto. Instead, we rely // on seeing sawTransforms=true and a call to extractTransforms. { desc: "Transforms in map are ignored, other values are kept (ServerTimestamp)", in: map[string]interface{}{"a": ServerTimestamp, "b": 5}, want: mapval(map[string]*pb.Value{"b": intval(5)}), }, { desc: "Transforms in map are ignored, other values are kept (ArrayUnion)", in: map[string]interface{}{"a": ArrayUnion(1, 2, 3), "b": 5}, want: mapval(map[string]*pb.Value{"b": intval(5)}), }, { desc: "Transforms in map are ignored, other values are kept (ArrayRemove)", in: map[string]interface{}{"a": ArrayRemove(1, 2, 3), "b": 5}, want: mapval(map[string]*pb.Value{"b": intval(5)}), }, } { t.Run(test.desc, func(t *testing.T) { got, _, err := toProtoValue(reflect.ValueOf(test.in)) if err != nil { t.Fatalf("%v (%T): %v", test.in, test.in, err) } if !testEqual(got, test.want) { t.Fatalf("%+v (%T):\ngot\n%+v\nwant\n%+v", test.in, test.in, got, test.want) } }) } } type stringy struct{} func (stringy) String() string { return "stringy" } func TestToProtoValue_Errors(t *testing.T) { for _, in := range []interface{}{ uint64(0), // a bad fit for int64 map[int]bool{}, // map key type is not string make(chan int), // can't handle type map[string]fmt.Stringer{"a": stringy{}}, // only empty interfaces ServerTimestamp, // ServerTimestamp can only be a field value struct{ A interface{} }{A: ServerTimestamp}, map[string]interface{}{"a": []interface{}{ServerTimestamp}}, map[string]interface{}{"a": []interface{}{ map[string]interface{}{"b": ServerTimestamp}, }}, Delete, // Delete should never appear []interface{}{Delete}, map[string]interface{}{"a": Delete}, map[string]interface{}{"a": []interface{}{Delete}}, // Transforms are not allowed to occur in an array. []interface{}{ServerTimestamp}, []interface{}{ArrayUnion(1, 2, 3)}, []interface{}{ArrayRemove(1, 2, 3)}, // Transforms are not allowed to occur in a struct. struct{ A interface{} }{A: ServerTimestamp}, struct{ A interface{} }{A: ArrayUnion()}, struct{ A interface{} }{A: ArrayRemove()}, } { _, _, err := toProtoValue(reflect.ValueOf(in)) if err == nil { t.Errorf("%v: got nil, want error", in) } } } func TestToProtoValue_SawTransform(t *testing.T) { for i, in := range []interface{}{ map[string]interface{}{"a": ServerTimestamp}, map[string]interface{}{"a": ArrayUnion()}, map[string]interface{}{"a": ArrayRemove()}, } { _, sawTransform, err := toProtoValue(reflect.ValueOf(in)) if err != nil { t.Fatalf("%d %v: got err %v\nexpected nil", i, in, err) } if !sawTransform { t.Errorf("%d %v: got sawTransform=false, expected sawTransform=true", i, in) } } } type testStruct2 struct { Ignore int `firestore:"-"` Rename int `firestore:"a"` OmitEmpty int `firestore:",omitempty"` OmitEmptyTime time.Time `firestore:",omitempty"` } func TestToProtoValue_Tags(t *testing.T) { in := &testStruct2{ Ignore: 1, Rename: 2, OmitEmpty: 3, OmitEmptyTime: aTime, } got, _, err := toProtoValue(reflect.ValueOf(in)) if err != nil { t.Fatal(err) } want := mapval(map[string]*pb.Value{ "a": intval(2), "OmitEmpty": intval(3), "OmitEmptyTime": tsval(aTime), }) if !testEqual(got, want) { t.Errorf("got %+v, want %+v", got, want) } got, _, err = toProtoValue(reflect.ValueOf(testStruct2{})) if err != nil { t.Fatal(err) } want = mapval(map[string]*pb.Value{"a": intval(0)}) if !testEqual(got, want) { t.Errorf("got\n%+v\nwant\n%+v", got, want) } } func TestToProtoValue_Embedded(t *testing.T) { // Embedded time.Time, LatLng, or Timestamp should behave like non-embedded. type embed struct { time.Time *latlng.LatLng *ts.Timestamp } got, _, err := toProtoValue(reflect.ValueOf(embed{tm, ll, ptm})) if err != nil { t.Fatal(err) } want := mapval(map[string]*pb.Value{ "Time": tsval(tm), "LatLng": geoval(ll), "Timestamp": {ValueType: &pb.Value_TimestampValue{ptm}}, }) if !testEqual(got, want) { t.Errorf("got %+v, want %+v", got, want) } } func TestIsEmpty(t *testing.T) { for _, e := range []interface{}{int(0), float32(0), false, "", []int{}, []int(nil), (*int)(nil)} { if !isEmptyValue(reflect.ValueOf(e)) { t.Errorf("%v (%T): want true, got false", e, e) } } i := 3 for _, n := range []interface{}{int(1), float32(1), true, "x", []int{1}, &i} { if isEmptyValue(reflect.ValueOf(n)) { t.Errorf("%v (%T): want false, got true", n, n) } } }