// 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. // A runner for the conformance tests. package firestore import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" "math" "path" "path/filepath" "strings" "testing" "time" pb "cloud.google.com/go/firestore/internal/conformance" "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" ts "github.com/golang/protobuf/ptypes/timestamp" "github.com/google/go-cmp/cmp" "google.golang.org/api/iterator" fspb "google.golang.org/genproto/googleapis/firestore/v1" ) func TestConformance(t *testing.T) { dir := "internal/conformance/testdata" files, err := ioutil.ReadDir(dir) if err != nil { t.Fatal(err) } wtid := watchTargetID watchTargetID = 1 defer func() { watchTargetID = wtid }() for _, f := range files { if !strings.Contains(f.Name(), ".json") { continue } inBytes, err := ioutil.ReadFile(filepath.Join(dir, f.Name())) if err != nil { t.Fatalf("%s: %v", f.Name(), err) } var tf pb.TestFile if err := jsonpb.Unmarshal(bytes.NewReader(inBytes), &tf); err != nil { t.Fatalf("unmarshalling %s: %v", f.Name(), err) } for _, tc := range tf.Tests { t.Run(tc.Description, func(t *testing.T) { c, srv, cleanup := newMock(t) defer cleanup() if err := runTest(tc, c, srv); err != nil { t.Fatal(err) } }) } } } func runTest(test *pb.Test, c *Client, srv *mockServer) error { check := func(gotErr error, wantErr bool) error { if wantErr && gotErr == nil { return errors.New("got nil, want error") } if !wantErr && gotErr != nil { return gotErr } return nil } ctx := context.Background() switch typedTestcase := test.Test.(type) { case *pb.Test_Get: req := &fspb.BatchGetDocumentsRequest{ Database: c.path(), Documents: []string{typedTestcase.Get.DocRefPath}, } srv.addRPC(req, []interface{}{ &fspb.BatchGetDocumentsResponse{ Result: &fspb.BatchGetDocumentsResponse_Found{&fspb.Document{ Name: typedTestcase.Get.DocRefPath, CreateTime: &ts.Timestamp{}, UpdateTime: &ts.Timestamp{}, }}, ReadTime: &ts.Timestamp{}, }, }) ref := docRefFromPath(typedTestcase.Get.DocRefPath, c) _, err := ref.Get(ctx) if err != nil { return err } // Checking response would just be testing the function converting a Document // proto to a DocumentSnapshot, hence uninteresting. case *pb.Test_Create: srv.addRPC(typedTestcase.Create.Request, commitResponseForSet) ref := docRefFromPath(typedTestcase.Create.DocRefPath, c) data, err := convertData(typedTestcase.Create.JsonData) if err != nil { return err } _, checkErr := ref.Create(ctx, data) if err := check(checkErr, typedTestcase.Create.IsError); err != nil { return err } case *pb.Test_Set: srv.addRPC(typedTestcase.Set.Request, commitResponseForSet) ref := docRefFromPath(typedTestcase.Set.DocRefPath, c) data, err := convertData(typedTestcase.Set.JsonData) if err != nil { return err } var opts []SetOption if typedTestcase.Set.Option != nil { opts = []SetOption{convertSetOption(typedTestcase.Set.Option)} } _, checkErr := ref.Set(ctx, data, opts...) if err := check(checkErr, typedTestcase.Set.IsError); err != nil { return err } case *pb.Test_Update: // Ignore Update test because we only support UpdatePaths. // Not to worry, every Update test has a corresponding UpdatePaths test. case *pb.Test_UpdatePaths: srv.addRPC(typedTestcase.UpdatePaths.Request, commitResponseForSet) ref := docRefFromPath(typedTestcase.UpdatePaths.DocRefPath, c) preconds, err := convertPrecondition(typedTestcase.UpdatePaths.Precondition) if err != nil { return err } paths := convertFieldPaths(typedTestcase.UpdatePaths.FieldPaths) var ups []Update for i, p := range paths { val, err := convertJSONValue(typedTestcase.UpdatePaths.JsonValues[i]) if err != nil { return err } ups = append(ups, Update{ FieldPath: p, Value: val, }) } _, checkErr := ref.Update(ctx, ups, preconds...) if err := check(checkErr, typedTestcase.UpdatePaths.IsError); err != nil { return err } case *pb.Test_Delete: srv.addRPC(typedTestcase.Delete.Request, commitResponseForSet) ref := docRefFromPath(typedTestcase.Delete.DocRefPath, c) preconds, err := convertPrecondition(typedTestcase.Delete.Precondition) if err != nil { return err } _, checkErr := ref.Delete(ctx, preconds...) if err := check(checkErr, typedTestcase.Delete.IsError); err != nil { return err } case *pb.Test_Query: q, err := convertQuery(typedTestcase.Query) if err != nil { return err } got, checkErr := q.toProto() if err := check(checkErr, typedTestcase.Query.IsError); err == nil && checkErr == nil { if want := typedTestcase.Query.Query; !proto.Equal(got, want) { return fmt.Errorf("got: %s\nwant: %s", proto.MarshalTextString(got), proto.MarshalTextString(want)) } } case *pb.Test_Listen: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() iter := c.Collection("C").OrderBy("a", Asc).Snapshots(ctx) var rs []interface{} for _, r := range typedTestcase.Listen.Responses { rs = append(rs, r) } srv.addRPC(&fspb.ListenRequest{ Database: "projects/projectID/databases/(default)", TargetChange: &fspb.ListenRequest_AddTarget{iter.ws.target}, }, rs) got, err := nSnapshots(iter, len(typedTestcase.Listen.Snapshots)) if err != nil { return err } else if diff := cmp.Diff(got, typedTestcase.Listen.Snapshots); diff != "" { return errors.New(diff) } if typedTestcase.Listen.IsError { _, err := iter.Next() if err == nil { return fmt.Errorf("got nil, want error") } } default: return fmt.Errorf("unknown test type %T", typedTestcase) } return nil } func nSnapshots(iter *QuerySnapshotIterator, n int) ([]*pb.Snapshot, error) { var snaps []*pb.Snapshot for i := 0; i < n; i++ { qsnap, err := iter.Next() if err != nil { return snaps, err } s := &pb.Snapshot{ReadTime: mustTimestampProto(qsnap.ReadTime)} for { doc, err := qsnap.Documents.Next() if err == iterator.Done { break } if err != nil { return snaps, err } s.Docs = append(s.Docs, doc.proto) } for _, c := range qsnap.Changes { var k pb.DocChange_Kind switch c.Kind { case DocumentAdded: k = pb.DocChange_ADDED case DocumentRemoved: k = pb.DocChange_REMOVED case DocumentModified: k = pb.DocChange_MODIFIED default: panic("bad kind") } s.Changes = append(s.Changes, &pb.DocChange{ Kind: k, Doc: c.Doc.proto, OldIndex: int32(c.OldIndex), NewIndex: int32(c.NewIndex), }) } snaps = append(snaps, s) } return snaps, nil } func docRefFromPath(p string, c *Client) *DocumentRef { return &DocumentRef{ Path: p, ID: path.Base(p), Parent: &CollectionRef{c: c}, } } func convertJSONValue(jv string) (interface{}, error) { var val interface{} if err := json.Unmarshal([]byte(jv), &val); err != nil { return nil, err } return convertTestValue(val), nil } func convertData(jsonData string) (map[string]interface{}, error) { var m map[string]interface{} if err := json.Unmarshal([]byte(jsonData), &m); err != nil { return nil, err } return convertTestMap(m), nil } func convertTestMap(m map[string]interface{}) map[string]interface{} { for k, v := range m { m[k] = convertTestValue(v) } return m } func convertTestValue(v interface{}) interface{} { switch v := v.(type) { case string: switch v { case "ServerTimestamp": return ServerTimestamp case "Delete": return Delete case "NaN": return math.NaN() default: return v } case float64: if v == float64(int(v)) { return int(v) } return v case []interface{}: if len(v) > 0 { if fv, ok := v[0].(string); ok { if fv == "ArrayUnion" { return ArrayUnion(convertTestValue(v[1:]).([]interface{})...) } if fv == "ArrayRemove" { return ArrayRemove(convertTestValue(v[1:]).([]interface{})...) } } } for i, e := range v { v[i] = convertTestValue(e) } return v case map[string]interface{}: return convertTestMap(v) default: return v } } func convertSetOption(opt *pb.SetOption) SetOption { if opt.All { return MergeAll } return Merge(convertFieldPaths(opt.Fields)...) } func convertFieldPaths(fps []*pb.FieldPath) []FieldPath { var res []FieldPath for _, fp := range fps { res = append(res, fp.Field) } return res } func convertPrecondition(fp *fspb.Precondition) ([]Precondition, error) { if fp == nil { return nil, nil } var pc Precondition switch fp := fp.ConditionType.(type) { case *fspb.Precondition_Exists: pc = exists(fp.Exists) case *fspb.Precondition_UpdateTime: tm, err := ptypes.Timestamp(fp.UpdateTime) if err != nil { return nil, err } pc = LastUpdateTime(tm) default: return nil, fmt.Errorf("unknown precondition type %T", fp) } return []Precondition{pc}, nil } func convertQuery(qt *pb.QueryTest) (*Query, error) { parts := strings.Split(qt.CollPath, "/") q := Query{ parentPath: strings.Join(parts[:len(parts)-2], "/"), collectionID: parts[len(parts)-1], path: qt.CollPath, } for _, c := range qt.Clauses { switch c := c.Clause.(type) { case *pb.Clause_Select: q = q.SelectPaths(convertFieldPaths(c.Select.Fields)...) case *pb.Clause_OrderBy: var dir Direction switch c.OrderBy.Direction { case "asc": dir = Asc case "desc": dir = Desc default: return nil, fmt.Errorf("bad direction: %q", c.OrderBy.Direction) } q = q.OrderByPath(FieldPath(c.OrderBy.Path.Field), dir) case *pb.Clause_Where: val, err := convertJSONValue(c.Where.JsonValue) if err != nil { return nil, err } q = q.WherePath(FieldPath(c.Where.Path.Field), c.Where.Op, val) case *pb.Clause_Offset: q = q.Offset(int(c.Offset)) case *pb.Clause_Limit: q = q.Limit(int(c.Limit)) case *pb.Clause_StartAt: cs, err := convertCursor(c.StartAt) if err != nil { return nil, err } q = q.StartAt(cs...) case *pb.Clause_StartAfter: cs, err := convertCursor(c.StartAfter) if err != nil { return nil, err } q = q.StartAfter(cs...) case *pb.Clause_EndAt: cs, err := convertCursor(c.EndAt) if err != nil { return nil, err } q = q.EndAt(cs...) case *pb.Clause_EndBefore: cs, err := convertCursor(c.EndBefore) if err != nil { return nil, err } q = q.EndBefore(cs...) default: return nil, fmt.Errorf("bad clause type %T", c) } } return &q, nil } // Returns args to a cursor method (StartAt, etc.). func convertCursor(c *pb.Cursor) ([]interface{}, error) { if c.DocSnapshot != nil { ds, err := convertDocSnapshot(c.DocSnapshot) if err != nil { return nil, err } return []interface{}{ds}, nil } var vals []interface{} for _, jv := range c.JsonValues { v, err := convertJSONValue(jv) if err != nil { return nil, err } vals = append(vals, v) } return vals, nil } func convertDocSnapshot(ds *pb.DocSnapshot) (*DocumentSnapshot, error) { data, err := convertData(ds.JsonData) if err != nil { return nil, err } doc, transformPaths, err := toProtoDocument(data) if err != nil { return nil, err } if len(transformPaths) > 0 { return nil, errors.New("saw transform paths in DocSnapshot") } return &DocumentSnapshot{ Ref: &DocumentRef{ Path: ds.Path, Parent: &CollectionRef{Path: path.Dir(ds.Path)}, }, proto: doc, }, nil }