1// Copyright 2017 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 firestore
16
17import (
18	"context"
19	"reflect"
20	"sort"
21	"testing"
22	"time"
23
24	pb "google.golang.org/genproto/googleapis/firestore/v1"
25	"google.golang.org/genproto/googleapis/type/latlng"
26	"google.golang.org/grpc/codes"
27	"google.golang.org/grpc/status"
28)
29
30var (
31	writeResultForSet    = &WriteResult{UpdateTime: aTime}
32	commitResponseForSet = &pb.CommitResponse{
33		WriteResults: []*pb.WriteResult{{UpdateTime: aTimestamp}},
34	}
35)
36
37func TestDocGet(t *testing.T) {
38	ctx := context.Background()
39	c, srv, cleanup := newMock(t)
40	defer cleanup()
41
42	path := "projects/projectID/databases/(default)/documents/C/a"
43	pdoc := &pb.Document{
44		Name:       path,
45		CreateTime: aTimestamp,
46		UpdateTime: aTimestamp,
47		Fields:     map[string]*pb.Value{"f": intval(1)},
48	}
49	srv.addRPC(&pb.BatchGetDocumentsRequest{
50		Database:  c.path(),
51		Documents: []string{path},
52	}, []interface{}{
53		&pb.BatchGetDocumentsResponse{
54			Result:   &pb.BatchGetDocumentsResponse_Found{pdoc},
55			ReadTime: aTimestamp2,
56		},
57	})
58	ref := c.Collection("C").Doc("a")
59	gotDoc, err := ref.Get(ctx)
60	if err != nil {
61		t.Fatal(err)
62	}
63	wantDoc := &DocumentSnapshot{
64		Ref:        ref,
65		CreateTime: aTime,
66		UpdateTime: aTime,
67		ReadTime:   aTime2,
68		proto:      pdoc,
69		c:          c,
70	}
71	if !testEqual(gotDoc, wantDoc) {
72		t.Fatalf("\ngot  %+v\nwant %+v", gotDoc, wantDoc)
73	}
74
75	path2 := "projects/projectID/databases/(default)/documents/C/b"
76	srv.addRPC(
77		&pb.BatchGetDocumentsRequest{
78			Database:  c.path(),
79			Documents: []string{path2},
80		}, []interface{}{
81			&pb.BatchGetDocumentsResponse{
82				Result:   &pb.BatchGetDocumentsResponse_Missing{path2},
83				ReadTime: aTimestamp3,
84			},
85		})
86	_, err = c.Collection("C").Doc("b").Get(ctx)
87	if status.Code(err) != codes.NotFound {
88		t.Errorf("got %v, want NotFound", err)
89	}
90}
91
92func TestDocSet(t *testing.T) {
93	// Most tests for Set are in the conformance tests.
94	ctx := context.Background()
95	c, srv, cleanup := newMock(t)
96	defer cleanup()
97
98	doc := c.Collection("C").Doc("d")
99	// Merge with a struct and FieldPaths.
100	srv.addRPC(&pb.CommitRequest{
101		Database: "projects/projectID/databases/(default)",
102		Writes: []*pb.Write{
103			{
104				Operation: &pb.Write_Update{
105					Update: &pb.Document{
106						Name: "projects/projectID/databases/(default)/documents/C/d",
107						Fields: map[string]*pb.Value{
108							"*": mapval(map[string]*pb.Value{
109								"~": boolval(true),
110							}),
111						},
112					},
113				},
114				UpdateMask: &pb.DocumentMask{FieldPaths: []string{"`*`.`~`"}},
115			},
116		},
117	}, commitResponseForSet)
118	data := struct {
119		A map[string]bool `firestore:"*"`
120	}{A: map[string]bool{"~": true}}
121	wr, err := doc.Set(ctx, data, Merge([]string{"*", "~"}))
122	if err != nil {
123		t.Fatal(err)
124	}
125	if !testEqual(wr, writeResultForSet) {
126		t.Errorf("got %v, want %v", wr, writeResultForSet)
127	}
128
129	// MergeAll cannot be used with structs.
130	_, err = doc.Set(ctx, data, MergeAll)
131	if err == nil {
132		t.Errorf("got nil, want error")
133	}
134}
135
136func TestDocCreate(t *testing.T) {
137	// Verify creation with structs. In particular, make sure zero values
138	// are handled well.
139	// Other tests for Create are handled by the conformance tests.
140	ctx := context.Background()
141	c, srv, cleanup := newMock(t)
142	defer cleanup()
143
144	type create struct {
145		Time  time.Time
146		Bytes []byte
147		Geo   *latlng.LatLng
148	}
149	srv.addRPC(
150		&pb.CommitRequest{
151			Database: "projects/projectID/databases/(default)",
152			Writes: []*pb.Write{
153				{
154					Operation: &pb.Write_Update{
155						Update: &pb.Document{
156							Name: "projects/projectID/databases/(default)/documents/C/d",
157							Fields: map[string]*pb.Value{
158								"Time":  tsval(time.Time{}),
159								"Bytes": bytesval(nil),
160								"Geo":   nullValue,
161							},
162						},
163					},
164					CurrentDocument: &pb.Precondition{
165						ConditionType: &pb.Precondition_Exists{false},
166					},
167				},
168			},
169		},
170		commitResponseForSet,
171	)
172	_, err := c.Collection("C").Doc("d").Create(ctx, &create{})
173	if err != nil {
174		t.Fatal(err)
175	}
176}
177
178func TestDocDelete(t *testing.T) {
179	ctx := context.Background()
180	c, srv, cleanup := newMock(t)
181	defer cleanup()
182
183	srv.addRPC(
184		&pb.CommitRequest{
185			Database: "projects/projectID/databases/(default)",
186			Writes: []*pb.Write{
187				{Operation: &pb.Write_Delete{"projects/projectID/databases/(default)/documents/C/d"}},
188			},
189		},
190		&pb.CommitResponse{
191			WriteResults: []*pb.WriteResult{{}},
192		})
193	wr, err := c.Collection("C").Doc("d").Delete(ctx)
194	if err != nil {
195		t.Fatal(err)
196	}
197	if !testEqual(wr, &WriteResult{}) {
198		t.Errorf("got %+v, want %+v", wr, writeResultForSet)
199	}
200}
201
202var (
203	testData   = map[string]interface{}{"a": 1}
204	testFields = map[string]*pb.Value{"a": intval(1)}
205)
206
207// Update is tested by the conformance tests.
208
209func TestFPVsFromData(t *testing.T) {
210	type S struct{ X int }
211
212	for _, test := range []struct {
213		in   interface{}
214		want []fpv
215	}{
216		{
217			in:   nil,
218			want: []fpv{{nil, nil}},
219		},
220		{
221			in:   map[string]interface{}{"a": nil},
222			want: []fpv{{[]string{"a"}, nil}},
223		},
224		{
225			in:   map[string]interface{}{"a": 1},
226			want: []fpv{{[]string{"a"}, 1}},
227		},
228		{
229			in: map[string]interface{}{
230				"a": 1,
231				"b": map[string]interface{}{"c": 2},
232			},
233			want: []fpv{{[]string{"a"}, 1}, {[]string{"b", "c"}, 2}},
234		},
235		{
236			in:   map[string]interface{}{"s": &S{X: 3}},
237			want: []fpv{{[]string{"s"}, &S{X: 3}}},
238		},
239	} {
240		var got []fpv
241		fpvsFromData(reflect.ValueOf(test.in), nil, &got)
242		sort.Sort(byFieldPath(got))
243		if !testEqual(got, test.want) {
244			t.Errorf("%+v: got %v, want %v", test.in, got, test.want)
245		}
246	}
247}
248
249type byFieldPath []fpv
250
251func (b byFieldPath) Len() int           { return len(b) }
252func (b byFieldPath) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
253func (b byFieldPath) Less(i, j int) bool { return b[i].fieldPath.less(b[j].fieldPath) }
254
255func commitRequestForSet() *pb.CommitRequest {
256	return &pb.CommitRequest{
257		Database: "projects/projectID/databases/(default)",
258		Writes: []*pb.Write{
259			{
260				Operation: &pb.Write_Update{
261					Update: &pb.Document{
262						Name:   "projects/projectID/databases/(default)/documents/C/d",
263						Fields: testFields,
264					},
265				},
266			},
267		},
268	}
269}
270
271func TestUpdateProcess(t *testing.T) {
272	for _, test := range []struct {
273		in      Update
274		want    fpv
275		wantErr bool
276	}{
277		{
278			in:   Update{Path: "a", Value: 1},
279			want: fpv{fieldPath: []string{"a"}, value: 1},
280		},
281		{
282			in:   Update{Path: "c.d", Value: Delete},
283			want: fpv{fieldPath: []string{"c", "d"}, value: Delete},
284		},
285		{
286			in:   Update{FieldPath: []string{"*", "~"}, Value: ServerTimestamp},
287			want: fpv{fieldPath: []string{"*", "~"}, value: ServerTimestamp},
288		},
289		{
290			in:      Update{Path: "*"},
291			wantErr: true, // bad rune in path
292		},
293		{
294			in:      Update{Path: "a", FieldPath: []string{"b"}},
295			wantErr: true, // both Path and FieldPath
296		},
297		{
298			in:      Update{Value: 1},
299			wantErr: true, // neither Path nor FieldPath
300		},
301		{
302			in:      Update{FieldPath: []string{"", "a"}},
303			wantErr: true, // empty FieldPath component
304		},
305	} {
306		got, err := test.in.process()
307		if test.wantErr {
308			if err == nil {
309				t.Errorf("%+v: got nil, want error", test.in)
310			}
311		} else if err != nil {
312			t.Errorf("%+v: got error %v, want nil", test.in, err)
313		} else if !testEqual(got, test.want) {
314			t.Errorf("%+v: got %+v, want %+v", test.in, got, test.want)
315		}
316	}
317}
318