| // 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, |
| }, |
| }, |
| }, |
| }, |
| } |
| } |