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