// Copyright 2017 Google Inc. All Rights Reserved. // // 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 ( "reflect" "sort" "testing" "time" pb "google.golang.org/genproto/googleapis/firestore/v1beta1" "github.com/golang/protobuf/proto" "golang.org/x/net/context" "google.golang.org/genproto/googleapis/type/latlng" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) var ( writeResultForSet = &WriteResult{UpdateTime: aTime} commitResponseForSet = &pb.CommitResponse{ WriteResults: []*pb.WriteResult{{UpdateTime: aTimestamp}}, } ) func TestDocGet(t *testing.T) { ctx := context.Background() c, srv := newMock(t) path := "projects/projectID/databases/(default)/documents/C/a" pdoc := &pb.Document{ Name: path, CreateTime: aTimestamp, UpdateTime: aTimestamp, Fields: map[string]*pb.Value{"f": intval(1)}, } srv.addRPC(&pb.GetDocumentRequest{Name: path}, pdoc) ref := c.Collection("C").Doc("a") gotDoc, err := ref.Get(ctx) if err != nil { t.Fatal(err) } wantDoc := &DocumentSnapshot{ Ref: ref, CreateTime: aTime, UpdateTime: aTime, proto: pdoc, c: c, } if !testEqual(gotDoc, wantDoc) { t.Fatalf("\ngot %+v\nwant %+v", gotDoc, wantDoc) } srv.addRPC( &pb.GetDocumentRequest{ Name: "projects/projectID/databases/(default)/documents/C/b", }, grpc.Errorf(codes.NotFound, "not found"), ) _, err = c.Collection("C").Doc("b").Get(ctx) if grpc.Code(err) != codes.NotFound { t.Errorf("got %v, want NotFound", err) } } func TestDocSet(t *testing.T) { ctx := context.Background() c, srv := newMock(t) for _, test := range []struct { desc string data interface{} opt SetOption write map[string]*pb.Value mask []string transform []string isErr bool }{ { desc: "Set with no options", data: map[string]interface{}{"a": 1}, write: map[string]*pb.Value{"a": intval(1)}, }, { desc: "Merge with a field", data: map[string]interface{}{"a": 1, "b": 2}, opt: Merge("a"), write: map[string]*pb.Value{"a": intval(1)}, mask: []string{"a"}, }, { desc: "Merge field is not a leaf", data: map[string]interface{}{ "a": map[string]interface{}{"b": 1, "c": 2}, "d": 3, }, opt: Merge("a"), write: map[string]*pb.Value{"a": mapval(map[string]*pb.Value{ "b": intval(1), "c": intval(2), })}, mask: []string{"a"}, }, { desc: "MergeAll", data: map[string]interface{}{"a": 1, "b": 2}, opt: MergeAll, write: map[string]*pb.Value{"a": intval(1), "b": intval(2)}, mask: []string{"a", "b"}, }, { desc: "MergeAll with nested fields", data: map[string]interface{}{ "a": 1, "b": map[string]interface{}{"c": 2}, }, opt: MergeAll, write: map[string]*pb.Value{ "a": intval(1), "b": mapval(map[string]*pb.Value{"c": intval(2)}), }, mask: []string{"a", "b.c"}, }, { desc: "Merge with FieldPaths", data: map[string]interface{}{"*": map[string]interface{}{"~": true}}, opt: MergePaths([]string{"*", "~"}), write: map[string]*pb.Value{ "*": mapval(map[string]*pb.Value{ "~": boolval(true), }), }, mask: []string{"`*`.`~`"}, }, { desc: "Merge with a struct and FieldPaths", data: struct { A map[string]bool `firestore:"*"` }{A: map[string]bool{"~": true}}, opt: MergePaths([]string{"*", "~"}), write: map[string]*pb.Value{ "*": mapval(map[string]*pb.Value{ "~": boolval(true), }), }, mask: []string{"`*`.`~`"}, }, { desc: "a ServerTimestamp field becomes a transform", data: map[string]interface{}{"a": 1, "b": ServerTimestamp}, write: map[string]*pb.Value{"a": intval(1)}, transform: []string{"b"}, }, { desc: "a ServerTimestamp alone", data: map[string]interface{}{"b": ServerTimestamp}, write: nil, transform: []string{"b"}, }, { desc: "a ServerTimestamp alone with a path", data: map[string]interface{}{"b": ServerTimestamp}, opt: MergePaths([]string{"b"}), write: nil, transform: []string{"b"}, }, { desc: "nested ServerTimestamp field", data: map[string]interface{}{ "a": 1, "b": map[string]interface{}{"c": ServerTimestamp}, }, write: map[string]*pb.Value{"a": intval(1)}, transform: []string{"b.c"}, }, { desc: "multiple ServerTimestamp fields", data: map[string]interface{}{ "a": 1, "b": ServerTimestamp, "c": map[string]interface{}{"d": ServerTimestamp}, }, write: map[string]*pb.Value{"a": intval(1)}, transform: []string{"b", "c.d"}, }, { desc: "ServerTimestamp with MergeAll", data: map[string]interface{}{"a": 1, "b": ServerTimestamp}, opt: MergeAll, write: map[string]*pb.Value{"a": intval(1)}, mask: []string{"a"}, transform: []string{"b"}, }, { desc: "ServerTimestamp with Merge of both fields", data: map[string]interface{}{"a": 1, "b": ServerTimestamp}, opt: Merge("a", "b"), write: map[string]*pb.Value{"a": intval(1)}, mask: []string{"a"}, transform: []string{"b"}, }, { desc: "If is ServerTimestamp not in Merge, no transform", data: map[string]interface{}{"a": 1, "b": ServerTimestamp}, opt: Merge("a"), write: map[string]*pb.Value{"a": intval(1)}, mask: []string{"a"}, }, { desc: "If no ordinary values in Merge, no write", data: map[string]interface{}{"a": 1, "b": ServerTimestamp}, opt: Merge("b"), transform: []string{"b"}, }, { desc: "Merge fields must all be present in data.", data: map[string]interface{}{"a": 1}, opt: Merge("b", "a"), isErr: true, }, { desc: "MergeAll cannot be used with structs", data: struct{ A int }{A: 1}, opt: MergeAll, isErr: true, }, { desc: "Delete cannot appear in data", data: map[string]interface{}{"a": 1, "b": Delete}, isErr: true, }, { desc: "Delete cannot even appear in an unmerged field (allow?)", data: map[string]interface{}{"a": 1, "b": Delete}, opt: Merge("a"), isErr: true, }, } { srv.reset() if !test.isErr { var writes []*pb.Write if test.write != nil || test.mask != nil { w := &pb.Write{} if test.write != nil { w.Operation = &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: test.write, }, } } if test.mask != nil { w.UpdateMask = &pb.DocumentMask{FieldPaths: test.mask} } writes = append(writes, w) } if test.transform != nil { var fts []*pb.DocumentTransform_FieldTransform for _, p := range test.transform { fts = append(fts, &pb.DocumentTransform_FieldTransform{ FieldPath: p, TransformType: requestTimeTransform, }) } writes = append(writes, &pb.Write{ Operation: &pb.Write_Transform{ &pb.DocumentTransform{ Document: "projects/projectID/databases/(default)/documents/C/d", FieldTransforms: fts, }, }, }) } srv.addRPC(&pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: writes, }, commitResponseForSet) } var opts []SetOption if test.opt != nil { opts = []SetOption{test.opt} } wr, err := c.Collection("C").Doc("d").Set(ctx, test.data, opts...) if test.isErr && err == nil { t.Errorf("%s: got nil, want error") continue } if !test.isErr && err != nil { t.Errorf("%s: %v", test.desc, err) continue } if err == nil && !testEqual(wr, writeResultForSet) { t.Errorf("%s: got %v, want %v", test.desc, wr, writeResultForSet) } } } func TestDocCreate(t *testing.T) { ctx := context.Background() c, srv := newMock(t) wantReq := commitRequestForSet() wantReq.Writes[0].CurrentDocument = &pb.Precondition{ ConditionType: &pb.Precondition_Exists{false}, } srv.addRPC(wantReq, commitResponseForSet) wr, err := c.Collection("C").Doc("d").Create(ctx, testData) if err != nil { t.Fatal(err) } if !testEqual(wr, writeResultForSet) { t.Errorf("got %v, want %v", wr, writeResultForSet) } // Verify creation with structs. In particular, make sure zero values // are handled well. type create struct { Time time.Time Bytes []byte Geo *latlng.LatLng } srv.addRPC( &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{ { Operation: &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: map[string]*pb.Value{ "Time": tsval(time.Time{}), "Bytes": bytesval(nil), "Geo": nullValue, }, }, }, CurrentDocument: &pb.Precondition{ ConditionType: &pb.Precondition_Exists{false}, }, }, }, }, commitResponseForSet, ) _, err = c.Collection("C").Doc("d").Create(ctx, &create{}) if err != nil { t.Fatal(err) } } func TestDocDelete(t *testing.T) { ctx := context.Background() c, srv := newMock(t) srv.addRPC( &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{ {Operation: &pb.Write_Delete{"projects/projectID/databases/(default)/documents/C/d"}}, }, }, &pb.CommitResponse{ WriteResults: []*pb.WriteResult{{}}, }) wr, err := c.Collection("C").Doc("d").Delete(ctx) if err != nil { t.Fatal(err) } if !testEqual(wr, &WriteResult{}) { t.Errorf("got %+v, want %+v", wr, writeResultForSet) } } func TestDocDeleteLastUpdateTime(t *testing.T) { ctx := context.Background() c, srv := newMock(t) wantReq := &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{ { Operation: &pb.Write_Delete{"projects/projectID/databases/(default)/documents/C/d"}, CurrentDocument: &pb.Precondition{ ConditionType: &pb.Precondition_UpdateTime{aTimestamp2}, }, }}, } srv.addRPC(wantReq, commitResponseForSet) wr, err := c.Collection("C").Doc("d").Delete(ctx, LastUpdateTime(aTime2)) if err != nil { t.Fatal(err) } if !testEqual(wr, writeResultForSet) { t.Errorf("got %+v, want %+v", wr, writeResultForSet) } } var ( testData = map[string]interface{}{"a": 1} testFields = map[string]*pb.Value{"a": intval(1)} ) func TestUpdateMap(t *testing.T) { ctx := context.Background() c, srv := newMock(t) for _, test := range []struct { data map[string]interface{} wantFields map[string]*pb.Value wantPaths []string }{ { data: map[string]interface{}{"a.b": 1}, wantFields: map[string]*pb.Value{ "a": mapval(map[string]*pb.Value{"b": intval(1)}), }, wantPaths: []string{"a.b"}, }, { data: map[string]interface{}{ "a": 1, "b": Delete, }, wantFields: map[string]*pb.Value{"a": intval(1)}, wantPaths: []string{"a", "b"}, }, } { srv.reset() wantReq := &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{{ Operation: &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: test.wantFields, }}, UpdateMask: &pb.DocumentMask{FieldPaths: test.wantPaths}, CurrentDocument: &pb.Precondition{ ConditionType: &pb.Precondition_Exists{true}, }, }}, } // Sort update masks, because map iteration order is random. sort.Strings(wantReq.Writes[0].UpdateMask.FieldPaths) srv.addRPCAdjust(wantReq, commitResponseForSet, func(gotReq proto.Message) { sort.Strings(gotReq.(*pb.CommitRequest).Writes[0].UpdateMask.FieldPaths) }) wr, err := c.Collection("C").Doc("d").UpdateMap(ctx, test.data) if err != nil { t.Fatal(err) } if !testEqual(wr, writeResultForSet) { t.Errorf("%v:\ngot %+v, want %+v", test.data, wr, writeResultForSet) } } } func TestUpdateMapLastUpdateTime(t *testing.T) { ctx := context.Background() c, srv := newMock(t) wantReq := &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{{ Operation: &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: map[string]*pb.Value{"a": intval(1)}, }}, UpdateMask: &pb.DocumentMask{FieldPaths: []string{"a"}}, CurrentDocument: &pb.Precondition{ ConditionType: &pb.Precondition_UpdateTime{aTimestamp2}, }, }}, } srv.addRPC(wantReq, commitResponseForSet) wr, err := c.Collection("C").Doc("d").UpdateMap(ctx, map[string]interface{}{"a": 1}, LastUpdateTime(aTime2)) if err != nil { t.Fatal(err) } if !testEqual(wr, writeResultForSet) { t.Errorf("got %v, want %v", wr, writeResultForSet) } } func TestUpdateMapErrors(t *testing.T) { ctx := context.Background() c, _ := newMock(t) for _, in := range []map[string]interface{}{ nil, // no paths map[string]interface{}{"a~b": 1}, // invalid character map[string]interface{}{"a..b": 1}, // empty path component map[string]interface{}{"a.b": 1, "a": 2}, // prefix } { _, err := c.Collection("C").Doc("d").UpdateMap(ctx, in) if err == nil { t.Errorf("%v: got nil, want error", in) } } } func TestUpdateStruct(t *testing.T) { type update struct{ A int } c, srv := newMock(t) wantReq := &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{{ Operation: &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: map[string]*pb.Value{"A": intval(2)}, }, }, UpdateMask: &pb.DocumentMask{FieldPaths: []string{"A", "b.c"}}, CurrentDocument: &pb.Precondition{ ConditionType: &pb.Precondition_Exists{true}, }, }}, } srv.addRPC(wantReq, commitResponseForSet) wr, err := c.Collection("C").Doc("d"). UpdateStruct(context.Background(), []string{"A", "b.c"}, &update{A: 2}) if err != nil { t.Fatal(err) } if !testEqual(wr, writeResultForSet) { t.Errorf("got %+v, want %+v", wr, writeResultForSet) } } func TestUpdateStructErrors(t *testing.T) { type update struct{ A int } ctx := context.Background() c, _ := newMock(t) doc := c.Collection("C").Doc("d") for _, test := range []struct { desc string fields []string data interface{} }{ { desc: "data is not a struct or *struct", data: map[string]interface{}{"a": 1}, }, { desc: "no paths", fields: nil, data: update{}, }, { desc: "empty", fields: []string{""}, data: update{}, }, { desc: "empty component", fields: []string{"a.b..c"}, data: update{}, }, { desc: "duplicate field", fields: []string{"a", "b", "c", "a"}, data: update{}, }, { desc: "invalid character", fields: []string{"a", "b]"}, data: update{}, }, { desc: "prefix", fields: []string{"a", "b", "c", "b.c"}, data: update{}, }, } { _, err := doc.UpdateStruct(ctx, test.fields, test.data) if err == nil { t.Errorf("%s: got nil, want error", test.desc) } } } func TestUpdatePaths(t *testing.T) { ctx := context.Background() c, srv := newMock(t) for _, test := range []struct { data []FieldPathUpdate wantFields map[string]*pb.Value wantPaths []string }{ { data: []FieldPathUpdate{ {Path: []string{"*", "~"}, Value: 1}, {Path: []string{"*", "/"}, Value: 2}, }, wantFields: map[string]*pb.Value{ "*": mapval(map[string]*pb.Value{ "~": intval(1), "/": intval(2), }), }, wantPaths: []string{"`*`.`~`", "`*`.`/`"}, }, { data: []FieldPathUpdate{ {Path: []string{"*"}, Value: 1}, {Path: []string{"]"}, Value: Delete}, }, wantFields: map[string]*pb.Value{"*": intval(1)}, wantPaths: []string{"`*`", "`]`"}, }, } { srv.reset() wantReq := &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{{ Operation: &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: test.wantFields, }}, UpdateMask: &pb.DocumentMask{FieldPaths: test.wantPaths}, CurrentDocument: &pb.Precondition{ ConditionType: &pb.Precondition_Exists{true}, }, }}, } // Sort update masks, because map iteration order is random. sort.Strings(wantReq.Writes[0].UpdateMask.FieldPaths) srv.addRPCAdjust(wantReq, commitResponseForSet, func(gotReq proto.Message) { sort.Strings(gotReq.(*pb.CommitRequest).Writes[0].UpdateMask.FieldPaths) }) wr, err := c.Collection("C").Doc("d").UpdatePaths(ctx, test.data) if err != nil { t.Fatal(err) } if !testEqual(wr, writeResultForSet) { t.Errorf("%v:\ngot %+v, want %+v", test.data, wr, writeResultForSet) } } } func TestUpdatePathsErrors(t *testing.T) { fpu := func(s ...string) FieldPathUpdate { return FieldPathUpdate{Path: s} } ctx := context.Background() c, _ := newMock(t) doc := c.Collection("C").Doc("d") for _, test := range []struct { desc string data []FieldPathUpdate }{ {"no updates", nil}, {"empty", []FieldPathUpdate{fpu("")}}, {"empty component", []FieldPathUpdate{fpu("*", "")}}, {"duplicate field", []FieldPathUpdate{fpu("~"), fpu("*"), fpu("~")}}, {"prefix", []FieldPathUpdate{fpu("*", "a"), fpu("b"), fpu("*", "a", "b")}}, } { _, err := doc.UpdatePaths(ctx, test.data) if err == nil { t.Errorf("%s: got nil, want error", test.desc) } } } func TestApplyFieldPaths(t *testing.T) { submap := mapval(map[string]*pb.Value{ "b": intval(1), "c": intval(2), }) fields := map[string]*pb.Value{ "a": submap, "d": intval(3), } for _, test := range []struct { fps []FieldPath want map[string]*pb.Value }{ {nil, nil}, {[]FieldPath{[]string{"z"}}, nil}, {[]FieldPath{[]string{"a"}}, map[string]*pb.Value{"a": submap}}, {[]FieldPath{[]string{"a", "b", "c"}}, nil}, {[]FieldPath{[]string{"d"}}, map[string]*pb.Value{"d": intval(3)}}, { []FieldPath{[]string{"d"}, []string{"a", "c"}}, map[string]*pb.Value{ "a": mapval(map[string]*pb.Value{"c": intval(2)}), "d": intval(3), }, }, } { got := applyFieldPaths(fields, test.fps, nil) if !testEqual(got, test.want) { t.Errorf("%v:\ngot %v\nwant \n%v", test.fps, got, test.want) } } } func TestFieldPathsFromMap(t *testing.T) { for _, test := range []struct { in map[string]interface{} want []string }{ {nil, nil}, {map[string]interface{}{"a": 1}, []string{"a"}}, {map[string]interface{}{ "a": 1, "b": map[string]interface{}{"c": 2}, }, []string{"a", "b.c"}}, } { fps := fieldPathsFromMap(reflect.ValueOf(test.in), nil) got := toServiceFieldPaths(fps) sort.Strings(got) if !testEqual(got, test.want) { t.Errorf("%+v: got %v, want %v", test.in, got, test.want) } } } func commitRequestForSet() *pb.CommitRequest { return &pb.CommitRequest{ Database: "projects/projectID/databases/(default)", Writes: []*pb.Write{ { Operation: &pb.Write_Update{ Update: &pb.Document{ Name: "projects/projectID/databases/(default)/documents/C/d", Fields: testFields, }, }, }, }, } }